Merge cb828d2357 into 2ef6e1386b
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@ src/build
|
|||
.ipynb_checkpoints/
|
||||
*build*/
|
||||
.cache/
|
||||
compare-skipmip-logs/
|
||||
|
|
|
|||
133
CMakeLists.txt
|
|
@ -151,6 +151,8 @@ add_library (pbrt_warnings INTERFACE)
|
|||
target_compile_options (
|
||||
pbrt_warnings
|
||||
INTERFACE
|
||||
# CCCL headers (CUDA 13.x): MSVC must use the conforming preprocessor when nvcc forwards to cl.exe.
|
||||
"$<$<CXX_COMPILER_ID:MSVC>:$<$<COMPILE_LANGUAGE:CUDA>:SHELL:-Xcompiler >/Zc:preprocessor>"
|
||||
"$<$<CXX_COMPILER_ID:MSVC>:$<$<COMPILE_LANGUAGE:CUDA>:SHELL:-Xcompiler >/wd4244>" # int -> float conversion
|
||||
"$<$<CXX_COMPILER_ID:MSVC>:$<$<COMPILE_LANGUAGE:CUDA>:SHELL:-Xcompiler >/wd4267>" # size_t -> int conversion
|
||||
"$<$<CXX_COMPILER_ID:MSVC>:$<$<COMPILE_LANGUAGE:CUDA>:SHELL:-Xcompiler >/wd4305>" # double constant assigned to float
|
||||
|
|
@ -211,6 +213,9 @@ of CUDA installed, please update your PATH.")
|
|||
message (WARNING "Found CUDA but PBRT_OPTIX_PATH is not set. Disabling GPU compilation.")
|
||||
else ()
|
||||
enable_language (CUDA)
|
||||
if (POLICY CMP0104)
|
||||
cmake_policy (SET CMP0104 NEW)
|
||||
endif ()
|
||||
list (APPEND PBRT_DEFINITIONS "PBRT_BUILD_GPU_RENDERER")
|
||||
if (PBRT_NVTX)
|
||||
list (APPEND PBRT_DEFINITIONS "NVTX")
|
||||
|
|
@ -220,6 +225,16 @@ of CUDA installed, please update your PATH.")
|
|||
endif ()
|
||||
set (PBRT_CUDA_ENABLED ON)
|
||||
|
||||
# CUDA 13+ nvcc emits Nvvm IR (e.g. v114) newer than OptiX 9.0/9.1's embedded compiler
|
||||
# (expects ~v107), so optixModuleCreate fails with 7200 / COMPILE ERROR. Embed PTX instead.
|
||||
set (_pbrt_embed_optix_ir_default ON)
|
||||
if (CUDA_VERSION_MAJOR GREATER_EQUAL 13)
|
||||
set (_pbrt_embed_optix_ir_default OFF)
|
||||
endif ()
|
||||
option (PBRT_EMBED_OPTIX_IR
|
||||
"Embed OptiX IR for optix.cu (OFF: PTX; set ON only with an OptiX/SDK version that matches your CUDA nvcc IR)"
|
||||
${_pbrt_embed_optix_ir_default})
|
||||
|
||||
# FIXME
|
||||
include_directories (${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}) # for regular c++ compiles
|
||||
|
||||
|
|
@ -242,9 +257,13 @@ of CUDA installed, please update your PATH.")
|
|||
target_compile_options (
|
||||
cuda_build_configuration
|
||||
INTERFACE
|
||||
"$<$<COMPILE_LANGUAGE:CUDA>:--std=c++17;--use_fast_math;--expt-relaxed-constexpr;--extended-lambda;--forward-unknown-to-host-compiler>"
|
||||
# The "$<NOT:$<BOOL:$<TARGET_PROPERTY:CUDA_PTX_COMPILATION>>>" part is to not add debugging symbols when generating PTX files for OptiX; see https://github.com/mmp/pbrt-v4/issues/69#issuecomment-715499748.
|
||||
"$<$<COMPILE_LANGUAGE:CUDA>:$<IF:$<AND:$<CONFIG:Debug>,$<NOT:$<BOOL:$<TARGET_PROPERTY:CUDA_PTX_COMPILATION>>>>,-G;-g,-lineinfo;-maxrregcount;128>>"
|
||||
# Use SHELL: so MSVC CUDA targets do not split on ';' and splice cl flags (/EHsc /MP)
|
||||
# into the middle of the nvcc line (nvcc then errors: single input file required).
|
||||
# C++ standard comes from CMAKE_CUDA_STANDARD; only extra device flags here.
|
||||
"$<$<COMPILE_LANGUAGE:CUDA>:SHELL:--use_fast_math --expt-relaxed-constexpr --extended-lambda --forward-unknown-to-host-compiler>"
|
||||
# Skip device debug flags when emitting OptiX PTX or OptiX IR; see https://github.com/mmp/pbrt-v4/issues/69#issuecomment-715499748.
|
||||
# Commas inside $<IF:cond,then,else> must be escaped (\,) when cond uses $<AND:...> with multiple args.
|
||||
"$<$<COMPILE_LANGUAGE:CUDA>:$<IF:$<AND:$<CONFIG:Debug>\,$<NOT:$<BOOL:$<TARGET_PROPERTY:CUDA_PTX_COMPILATION>>>\,$<NOT:$<BOOL:$<TARGET_PROPERTY:CUDA_OPTIX_COMPILATION>>>>,SHELL:-G -g,SHELL:-lineinfo -maxrregcount 128>>"
|
||||
)
|
||||
|
||||
if (PBRT_GPU_SHADER_MODEL STREQUAL "")
|
||||
|
|
@ -270,14 +289,42 @@ of CUDA installed, please update your PATH.")
|
|||
if (NOT ${CUDA_RETURN_CODE} EQUAL 0)
|
||||
message (SEND_ERROR ${CHECK_CUDA_OUTPUT})
|
||||
else ()
|
||||
set(ARCH "${CHECK_CUDA_OUTPUT}")
|
||||
set (ARCH "${CHECK_CUDA_OUTPUT}")
|
||||
message (STATUS "Detected CUDA Architecture: ${ARCH}")
|
||||
string (APPEND CMAKE_CUDA_FLAGS " --gpu-architecture=${ARCH}")
|
||||
endif ()
|
||||
else ()
|
||||
set(ARCH "${PBRT_GPU_SHADER_MODEL}")
|
||||
set (ARCH "${PBRT_GPU_SHADER_MODEL}")
|
||||
message (STATUS "Specified CUDA Architecture: ${ARCH}")
|
||||
string (APPEND CMAKE_CUDA_FLAGS " --gpu-architecture=${ARCH}")
|
||||
endif ()
|
||||
|
||||
# OptiX device code: CUDA 11.7+ can compile to OptiX IR (CMake CUDA_OPTIX_COMPILATION,
|
||||
# nvcc --optix-ir), which avoids ptxas on PTX entirely. Newer nvcc still runs ptxas on
|
||||
# PTX when using --generate-code=...,code=[compute_XX], which breaks on _optix_*.
|
||||
unset (PBRT_CUDA_ARCH_NUMBER)
|
||||
if (ARCH MATCHES "^sm_([0-9]+)$")
|
||||
set (PBRT_CUDA_ARCH_NUMBER "${CMAKE_MATCH_1}")
|
||||
elseif (ARCH MATCHES "^compute_([0-9]+)$")
|
||||
set (PBRT_CUDA_ARCH_NUMBER "${CMAKE_MATCH_1}")
|
||||
elseif (ARCH)
|
||||
message (FATAL_ERROR
|
||||
"PBRT_GPU_SHADER_MODEL must look like sm_89 or compute_89 (got '${ARCH}')")
|
||||
endif ()
|
||||
if (PBRT_CUDA_ARCH_NUMBER)
|
||||
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.18")
|
||||
set (CMAKE_CUDA_ARCHITECTURES "${PBRT_CUDA_ARCH_NUMBER}")
|
||||
message (STATUS "CMAKE_CUDA_ARCHITECTURES: ${CMAKE_CUDA_ARCHITECTURES}")
|
||||
else ()
|
||||
string (APPEND CMAKE_CUDA_FLAGS " --gpu-architecture=${ARCH}")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# VS + CUDA_ARCHITECTURES OFF (see pbrt_lib): device link must still get --gpu-architecture.
|
||||
if (MSVC AND ${CMAKE_GENERATOR} MATCHES "^Visual Studio" AND PBRT_CUDA_ARCH_NUMBER)
|
||||
set (PBRT_MSVC_CUDA_GPU_ARCHITECTURE "sm_${PBRT_CUDA_ARCH_NUMBER}")
|
||||
configure_file (
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/pbrt_cuda_vs.props.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/pbrt_cuda_vs.props"
|
||||
@ONLY)
|
||||
endif ()
|
||||
|
||||
set (PBRT_CUDA_LIB cuda)
|
||||
|
|
@ -301,18 +348,42 @@ of CUDA installed, please update your PATH.")
|
|||
endif ()
|
||||
|
||||
# this macro defines cmake rules that execute the following four steps:
|
||||
# 1) compile the given cuda file ${cuda_file} to an intermediary PTX file
|
||||
# 1) compile the given cuda file ${cuda_file} to PTX or OptiX IR (.optixir)
|
||||
# 2) use the 'bin2c' tool (that comes with CUDA) to
|
||||
# create a second intermediary (.c-)file which defines a const string variable
|
||||
# (named '${c_var_name}') whose (constant) value is the PTX output
|
||||
# create a second intermediary (.c-)file which defines a const byte array variable
|
||||
# (named '${c_var_name}') whose value is the module output
|
||||
# from the previous step.
|
||||
# 3) compile the given .c file to an intermediary object file (why thus has
|
||||
# that PTX string 'embedded' as a global constant.
|
||||
# 4) assign the name of the intermediary .o file to the cmake variable
|
||||
# 'output_var', which can then be added to cmake targets.
|
||||
macro (cuda_compile_and_embed output_var cuda_file lib_name)
|
||||
set (_pbrt_cuda_117_plus FALSE)
|
||||
if (CUDA_VERSION_MAJOR GREATER 11 OR (CUDA_VERSION_MAJOR EQUAL 11 AND CUDA_VERSION_MINOR GREATER_EQUAL 7))
|
||||
set (_pbrt_cuda_117_plus TRUE)
|
||||
endif ()
|
||||
add_library ("${lib_name}" OBJECT "${cuda_file}")
|
||||
set_property (TARGET "${lib_name}" PROPERTY CUDA_PTX_COMPILATION ON)
|
||||
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.27" AND _pbrt_cuda_117_plus AND PBRT_EMBED_OPTIX_IR)
|
||||
set_property (TARGET "${lib_name}" PROPERTY CUDA_OPTIX_COMPILATION ON)
|
||||
message (STATUS "Embedding OptiX IR (not PTX) for ${cuda_file} (CUDA ${CUDA_VERSION_MAJOR}.${CUDA_VERSION_MINOR}+ / CMake 3.27+)")
|
||||
else ()
|
||||
set_property (TARGET "${lib_name}" PROPERTY CUDA_PTX_COMPILATION ON)
|
||||
message (STATUS "Embedding PTX for ${cuda_file} (PBRT_EMBED_OPTIX_IR=${PBRT_EMBED_OPTIX_IR})")
|
||||
# MSVC + nvcc: always use compute_N for embedded PTX. Visual Studio needs this to avoid
|
||||
# MSBuild splitting -gencode on commas; Ninja needs it too so we do not rely on
|
||||
# CUDA_ARCHITECTURES=N-virtual (fragile for newer architectures like sm_120).
|
||||
if (MSVC AND PBRT_CUDA_ARCH_NUMBER)
|
||||
set_property (TARGET "${lib_name}" PROPERTY CUDA_ARCHITECTURES OFF)
|
||||
target_compile_options ("${lib_name}" PRIVATE
|
||||
"$<$<COMPILE_LANGUAGE:CUDA>:SHELL:-gpu-architecture=compute_${PBRT_CUDA_ARCH_NUMBER}>")
|
||||
elseif (PBRT_CUDA_ARCH_NUMBER AND CMAKE_VERSION VERSION_GREATER_EQUAL "3.23")
|
||||
set_property (TARGET "${lib_name}" PROPERTY CUDA_ARCHITECTURES "${PBRT_CUDA_ARCH_NUMBER}-virtual")
|
||||
elseif (PBRT_CUDA_ARCH_NUMBER)
|
||||
target_compile_options ("${lib_name}" PRIVATE
|
||||
"$<$<COMPILE_LANGUAGE:CUDA>:SHELL:-gpu-architecture=compute_${PBRT_CUDA_ARCH_NUMBER}>")
|
||||
endif ()
|
||||
endif ()
|
||||
unset (_pbrt_cuda_117_plus)
|
||||
|
||||
# disable "extern declaration... is treated as a static definition" warning
|
||||
if (CUDA_VERSION_MAJOR EQUAL 11 AND CUDA_VERSION_MINOR LESS 2)
|
||||
|
|
@ -323,13 +394,10 @@ of CUDA installed, please update your PATH.")
|
|||
-Xcudafe=--display_error_number -Xcudafe=--diag_suppress=20044)
|
||||
endif ()
|
||||
|
||||
# CUDA integration in Visual Studio seems broken as even if "Use
|
||||
# Host Preprocessor Definitions" is checked, the host preprocessor
|
||||
# definitions are still not used when compiling device code.
|
||||
# To work around that, define the macros using --define-macro to
|
||||
# avoid CMake identifying those as macros and using the proper (but
|
||||
# broken) way of specifying them.
|
||||
if (${CMAKE_GENERATOR} MATCHES "^Visual Studio")
|
||||
# With MSVC as nvcc's host compiler, host preprocessor definitions are not
|
||||
# reliably applied to device code (VS IDE integration; same symptom with Ninja).
|
||||
# Pass PBRT_* macros via --define-macro instead of target_compile_definitions.
|
||||
if (MSVC)
|
||||
# As PBRT_DEBUG_BUILD is specified globally as a definition, we need to
|
||||
# manually add it due to the bug mentioned earlier and due to it
|
||||
# not being found in PBRT_DEFINITIONS.
|
||||
|
|
@ -347,6 +415,11 @@ of CUDA installed, please update your PATH.")
|
|||
target_include_directories ("${lib_name}" SYSTEM PRIVATE ${NANOVDB_INCLUDE})
|
||||
target_link_libraries ("${lib_name}" PRIVATE cuda_build_configuration pbrt_opt pbrt_warnings)
|
||||
add_dependencies ("${lib_name}" pbrt_soa_generated)
|
||||
# See generated build tree pbrt_cuda_vs.props (CMake configures from cmake/pbrt_cuda_vs.props.in).
|
||||
if (MSVC AND ${CMAKE_GENERATOR} MATCHES "Visual Studio" AND PBRT_CUDA_ARCH_NUMBER)
|
||||
set_target_properties ("${lib_name}" PROPERTIES
|
||||
VS_USER_PROPS "${CMAKE_CURRENT_BINARY_DIR}/pbrt_cuda_vs.props")
|
||||
endif ()
|
||||
set (c_var_name ${output_var})
|
||||
set (embedded_file ${cuda_file}.ptx_embedded.c)
|
||||
add_custom_command (
|
||||
|
|
@ -551,6 +624,7 @@ set (PBRT_SOURCE
|
|||
src/pbrt/ray.cpp
|
||||
src/pbrt/samplers.cpp
|
||||
src/pbrt/scene.cpp
|
||||
src/pbrt/texture_mip_preprocess.cpp
|
||||
src/pbrt/shapes.cpp
|
||||
src/pbrt/textures.cpp
|
||||
|
||||
|
|
@ -581,6 +655,7 @@ set (PBRT_SOURCE_HEADERS
|
|||
src/pbrt/ray.h
|
||||
src/pbrt/samplers.h
|
||||
src/pbrt/scene.h
|
||||
src/pbrt/texture_mip_preprocess.h
|
||||
src/pbrt/shapes.h
|
||||
src/pbrt/textures.h
|
||||
)
|
||||
|
|
@ -885,7 +960,15 @@ if (PBRT_CUDA_ENABLED AND PBRT_OPTIX_PATH)
|
|||
target_include_directories (pbrt_lib SYSTEM PUBLIC ${PBRT_OPTIX_PATH}/include)
|
||||
endif ()
|
||||
|
||||
target_compile_options (pbrt_lib PUBLIC ${PBRT_CXX_FLAGS})
|
||||
# With LANGUAGE CUDA .cpp files, PUBLIC MSVC flags (/EHsc, /MP) must not apply to nvcc;
|
||||
# they end up as bare nvcc args and break CUDA 13 + VS (fatal: single input file required).
|
||||
if (PBRT_CUDA_ENABLED)
|
||||
foreach (_pbrt_cxx_flag IN LISTS PBRT_CXX_FLAGS)
|
||||
target_compile_options (pbrt_lib PUBLIC "$<$<COMPILE_LANGUAGE:CXX>:${_pbrt_cxx_flag}>")
|
||||
endforeach ()
|
||||
else ()
|
||||
target_compile_options (pbrt_lib PUBLIC ${PBRT_CXX_FLAGS})
|
||||
endif ()
|
||||
|
||||
target_link_libraries (pbrt_lib PRIVATE OpenEXR::OpenEXR pbrt_warnings pbrt_opt $<$<BOOL:PBRT_CUDA_ENABLED>:cuda_build_configuration>)
|
||||
|
||||
|
|
@ -896,6 +979,18 @@ if (WIN32)
|
|||
set_target_properties (pbrt_lib PROPERTIES OUTPUT_NAME libpbrt)
|
||||
endif()
|
||||
|
||||
if (PBRT_CUDA_ENABLED AND MSVC AND ${CMAKE_GENERATOR} MATCHES "Visual Studio" AND PBRT_CUDA_ARCH_NUMBER)
|
||||
# CMake emits --generate-code=...,code=[compute_N,sm_N]. MSBuild splits CudaCompile
|
||||
# AdditionalOptions on commas, leaving bare /EHsc /MP as nvcc args (nvcc fatal).
|
||||
# OFF disables CMake's arch flags; --gpu-architecture=sm_N has no comma for MSBuild.
|
||||
# Build tree pbrt_cuda_vs.props (from .in) strips /EHsc /MP and appends device-link sm_N.
|
||||
set_target_properties (pbrt_lib PROPERTIES CUDA_ARCHITECTURES OFF)
|
||||
target_compile_options (pbrt_lib PRIVATE
|
||||
"$<$<COMPILE_LANGUAGE:CUDA>:SHELL:--gpu-architecture=sm_${PBRT_CUDA_ARCH_NUMBER}>")
|
||||
set_target_properties (pbrt_lib PROPERTIES
|
||||
VS_USER_PROPS "${CMAKE_CURRENT_BINARY_DIR}/pbrt_cuda_vs.props")
|
||||
endif ()
|
||||
|
||||
set (ALL_PBRT_LIBS
|
||||
pbrt_lib
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
|
|
|
|||
BIN
book-skipmip.png
Normal file
|
After Width: | Height: | Size: 374 B |
|
|
@ -27,12 +27,13 @@
|
|||
# Taken from https://github.com/robertmaynard/code-samples/blob/master/posts/cmake_ptx/bin2c_wrapper.cmake
|
||||
# Modified to take a custom name instead of using the object name.
|
||||
|
||||
set(file_contents)
|
||||
set(file_contents "#include <stddef.h>\n\n")
|
||||
foreach(obj ${OBJECTS})
|
||||
get_filename_component(obj_ext ${obj} EXT)
|
||||
get_filename_component(obj_dir ${obj} DIRECTORY)
|
||||
|
||||
if(obj_ext MATCHES ".ptx")
|
||||
string(TOLOWER "${obj_ext}" obj_ext_lower)
|
||||
if(obj_ext_lower STREQUAL ".ptx" OR obj_ext_lower STREQUAL ".optixir")
|
||||
set(args --name ${VAR_NAME} ${obj})
|
||||
execute_process(COMMAND "${BIN_TO_C_COMMAND}" ${args}
|
||||
WORKING_DIRECTORY ${obj_dir}
|
||||
|
|
@ -43,4 +44,5 @@ foreach(obj ${OBJECTS})
|
|||
set(file_contents "${file_contents} \n${output}")
|
||||
endif()
|
||||
endforeach()
|
||||
set(file_contents "${file_contents}\nconst size_t ${VAR_NAME}_SIZE = sizeof(${VAR_NAME});\n")
|
||||
file(WRITE "${OUTPUT}" "${file_contents}")
|
||||
|
|
|
|||
27
cmake/pbrt_cuda_vs.props.in
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated in build tree by CMake (@ONLY). MSBuild comma issues: see PBRT Wiki / CMakeLists. -->
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemDefinitionGroup>
|
||||
<CudaCompile>
|
||||
<MultiProcessorCompilation>false</MultiProcessorCompilation>
|
||||
</CudaCompile>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- CMake's vcxproj replaces CudaLink AdditionalOptions without inheriting earlier defaults;
|
||||
append arch before nvcc -dlink so RDC matches sm_N (fixes "no kernel image" at runtime). -->
|
||||
<Target Name="PbrtAppendCudaLinkArchitecture" BeforeTargets="CudaLink">
|
||||
<ItemGroup>
|
||||
<CudaLink>
|
||||
<AdditionalOptions>%(AdditionalOptions) --gpu-architecture=@PBRT_MSVC_CUDA_GPU_ARCHITECTURE@</AdditionalOptions>
|
||||
</CudaLink>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="PbrtStripMsvcFlagsFromNvccAdditionalOptions" BeforeTargets="CudaBuild">
|
||||
<ItemGroup>
|
||||
<CudaCompile>
|
||||
<AdditionalOptions>$([System.Text.RegularExpressions.Regex]::Replace('%(CudaCompile.AdditionalOptions)', '\s+/EHsc\s+/MP\s+', ' '))</AdditionalOptions>
|
||||
</CudaCompile>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
4
compare-skipmip.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
@echo off
|
||||
REM Run from repo root: compare-skipmip.bat "<scene.pbrt>" [<spp>]
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0compare-skipmip.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
530
compare-skipmip.ps1
Normal file
|
|
@ -0,0 +1,530 @@
|
|||
# Compare pbrt runs with and without --skipmip: full logs on disk + timing/memory summary.
|
||||
#
|
||||
# Usage (from repo root):
|
||||
# .\compare-skipmip.ps1 -Scene "path\to\scene.pbrt"
|
||||
# .\compare-skipmip.ps1 -Scene "path\to\scene.pbrt" -Spp 16
|
||||
# .\compare-skipmip.ps1 "scene.pbrt" 16
|
||||
# .\compare-skipmip.ps1 "scene.pbrt" 16 -ShowProgress --gpu
|
||||
# .\compare-skipmip.ps1 "scene.pbrt" -ShowProgress --gpu (--gpu is not parsed as Spp; use -Gpu or trailing --gpu)
|
||||
# Omit -Spp and any leading digits-only tail so pbrt.exe does not get --spp (scene Integrator "integer pixelsamples" applies).
|
||||
#
|
||||
# Render progress: stdout is written to the .txt log live while pbrt runs (poll ~8 Hz for console echo).
|
||||
# Optional -ExtraPbrtArgs is appended for both runs (e.g. --wavefront).
|
||||
# Pass -Gpu or a trailing --gpu (after other args) to add pbrt's --gpu for both runs.
|
||||
# By default, repetitive "Rendering:" lines are not echoed (full log files unchanged); use -ShowProgress for all lines.
|
||||
# SkipMip mip preprocess logs one summary line by default; use -VerboseMipPreprocess for per-texture/per-geometry lines (slow).
|
||||
# SSIM: compare_ssim.py needs pip install numpy scikit-image pillow (full-res skimage SSIM).
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, Position = 0)]
|
||||
[string] $Scene,
|
||||
|
||||
# Named only (no Position): so trailing tokens like --gpu go to RemainingArguments, not here.
|
||||
# Optional positional spp: first RemainingArguments token that is all-digits (>= 1) when -Spp omitted.
|
||||
[Parameter(Mandatory = $false)]
|
||||
$Spp = $null,
|
||||
|
||||
[string] $PbrtExe = "",
|
||||
[string] $LogDir = "",
|
||||
[Alias('Extra')]
|
||||
[string[]] $ExtraPbrtArgs = @(),
|
||||
|
||||
# If set, every "Rendering:" progress line is printed; otherwise only a short note (full log unchanged).
|
||||
[switch] $ShowProgress,
|
||||
|
||||
# If set, SkipMip run adds --verbose-mip-preprocess (large mip analysis log; default is quiet).
|
||||
[switch] $VerboseMipPreprocess,
|
||||
|
||||
# If set (or pass trailing --gpu), both runs invoke pbrt with --gpu.
|
||||
[switch] $Gpu,
|
||||
|
||||
# Catches e.g. trailing --gpu when not using -ExtraPbrtArgs; other tokens are forwarded to pbrt after --gpu.
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]] $RemainingArguments = @()
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Resolve-PbrtExe {
|
||||
param([string] $Explicit)
|
||||
if ($Explicit -and (Test-Path -LiteralPath $Explicit)) {
|
||||
return (Resolve-Path -LiteralPath $Explicit).Path
|
||||
}
|
||||
$repoRoot = $PSScriptRoot
|
||||
$candidates = @(
|
||||
(Join-Path $repoRoot "build-gpu\Release\pbrt.exe"),
|
||||
(Join-Path $repoRoot "build-gpu\Debug\pbrt.exe"),
|
||||
(Join-Path $repoRoot "build-gpu\pbrt.exe")
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (Test-Path -LiteralPath $c) { return (Resolve-Path -LiteralPath $c).Path }
|
||||
}
|
||||
throw "Could not find pbrt.exe. Pass -PbrtExe or build under build\Release\pbrt.exe."
|
||||
}
|
||||
|
||||
function Read-LogText([string] $Path) {
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return "" }
|
||||
return [System.IO.File]::ReadAllText($Path)
|
||||
}
|
||||
|
||||
function Match-One {
|
||||
param([string] $Text, [string] $Pattern)
|
||||
$m = [regex]::Match($Text, $Pattern, [System.Text.RegularExpressions.RegexOptions]::Multiline)
|
||||
if (-not $m.Success) { return $null }
|
||||
return $m.Groups[1].Value.Trim()
|
||||
}
|
||||
|
||||
function Convert-MemoryToBytes([string] $MemStr) {
|
||||
if ([string]::IsNullOrWhiteSpace($MemStr)) { return $null }
|
||||
$t = $MemStr.Trim() -replace '\s+', ' '
|
||||
if ($t -notmatch '^([\d.]+)\s+(kB|MiB|GiB)$') { return $null }
|
||||
$n = [double]$Matches[1]
|
||||
switch ($Matches[2]) {
|
||||
'kB' { return [long][math]::Round($n * 1024) }
|
||||
'MiB' { return [long][math]::Round($n * 1024 * 1024) }
|
||||
'GiB' { return [long][math]::Round($n * 1024 * 1024 * 1024) }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# RAM SkipMip saves vs Full Mip as % of Full Mip (positive = less RAM with SkipMip; negative = SkipMip used more).
|
||||
function Format-SavingsPercent([int64] $FullMipBytes, [int64] $SkipMipBytes) {
|
||||
if ($FullMipBytes -le 0) { return $null }
|
||||
$pct = 100.0 * ($FullMipBytes - $SkipMipBytes) / [double]$FullMipBytes
|
||||
if ([math]::Abs($pct) -lt 0.05) { return "0.0%" }
|
||||
return "{0:N1}%" -f $pct
|
||||
}
|
||||
|
||||
function Split-LineForDisplay([string] $Line, [int] $MaxLen = 96) {
|
||||
if ([string]::IsNullOrEmpty($Line)) { return @("") }
|
||||
if ($Line.Length -le $MaxLen) { return @($Line) }
|
||||
$out = New-Object System.Collections.Generic.List[string]
|
||||
$rest = $Line
|
||||
while ($rest.Length -gt $MaxLen) {
|
||||
$chunk = $MaxLen
|
||||
$space = $rest.LastIndexOf(' ', $MaxLen)
|
||||
if ($space -gt $MaxLen / 2) { $chunk = $space }
|
||||
$out.Add($rest.Substring(0, $chunk).TrimEnd())
|
||||
$rest = $rest.Substring($chunk).TrimStart()
|
||||
}
|
||||
if ($rest.Length -gt 0) { $out.Add($rest) }
|
||||
return , $out.ToArray()
|
||||
}
|
||||
|
||||
function Write-PbrtLineToHost([string] $Line, [switch] $ShowProgress) {
|
||||
if (-not $ShowProgress -and $Line -match '^\s*Rendering:') {
|
||||
return
|
||||
}
|
||||
if ($Line -match '^\[mip preprocess\] texture') {
|
||||
Write-Host ""
|
||||
}
|
||||
if ($Line -eq '--- stderr ---') {
|
||||
Write-Host ""
|
||||
}
|
||||
if ($Line -match '^\s*Statistics:') {
|
||||
Write-Host ""
|
||||
}
|
||||
if ($Line -match '^\s*Warning:') {
|
||||
$parts = Split-LineForDisplay $Line 100
|
||||
for ($i = 0; $i -lt $parts.Count; $i++) {
|
||||
if ($i -eq 0) { Write-Host $parts[$i] }
|
||||
else { Write-Host (" {0}" -f $parts[$i]) }
|
||||
}
|
||||
return
|
||||
}
|
||||
Write-Host $Line
|
||||
}
|
||||
|
||||
function Invoke-PbrtLogged {
|
||||
param(
|
||||
[string] $PbrtExe,
|
||||
[string[]] $Arguments,
|
||||
[string] $LogPath,
|
||||
[switch] $ShowProgress
|
||||
)
|
||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
# Stdout -> log file directly so the log grows live (render progress, etc.). Stderr -> temp,
|
||||
# then append (avoids mixing two writers on one file). Do not use `& pbrt 2>&1` (stderr
|
||||
# becomes ErrorRecord under $ErrorActionPreference Stop).
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
if (Test-Path -LiteralPath $LogPath) {
|
||||
Remove-Item -LiteralPath $LogPath -Force
|
||||
}
|
||||
$errTemp = Join-Path ([System.IO.Path]::GetTempPath()) ("pbrt-compare-err-" + [guid]::NewGuid() + ".txt")
|
||||
$proc = Start-Process -FilePath $PbrtExe -ArgumentList $Arguments -PassThru -NoNewWindow `
|
||||
-RedirectStandardOutput $LogPath -RedirectStandardError $errTemp
|
||||
|
||||
$seenLines = 0
|
||||
$nProgress = 0
|
||||
while (-not $proc.HasExited) {
|
||||
if (Test-Path -LiteralPath $LogPath) {
|
||||
try {
|
||||
$all = [System.IO.File]::ReadAllLines($LogPath, $utf8)
|
||||
} catch {
|
||||
$all = @()
|
||||
}
|
||||
while ($seenLines -lt $all.Length) {
|
||||
$line = $all[$seenLines]
|
||||
$seenLines++
|
||||
if ($line -match '^\s*Rendering:') { $nProgress++ }
|
||||
Write-PbrtLineToHost -Line $line -ShowProgress:$ShowProgress
|
||||
}
|
||||
}
|
||||
Start-Sleep -Milliseconds 120
|
||||
}
|
||||
$null = $proc.WaitForExit()
|
||||
$exitCode = $proc.ExitCode
|
||||
Start-Sleep -Milliseconds 80
|
||||
if (Test-Path -LiteralPath $LogPath) {
|
||||
try {
|
||||
$all = [System.IO.File]::ReadAllLines($LogPath, $utf8)
|
||||
} catch {
|
||||
$all = @()
|
||||
}
|
||||
while ($seenLines -lt $all.Length) {
|
||||
$line = $all[$seenLines]
|
||||
$seenLines++
|
||||
if ($line -match '^\s*Rendering:') { $nProgress++ }
|
||||
Write-PbrtLineToHost -Line $line -ShowProgress:$ShowProgress
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (Test-Path -LiteralPath $errTemp) {
|
||||
$errLines = [System.IO.File]::ReadAllLines($errTemp, $utf8)
|
||||
if ($errLines.Count -gt 0) {
|
||||
$swErr = New-Object System.IO.StreamWriter($LogPath, $true, $utf8)
|
||||
try {
|
||||
$swErr.WriteLine("")
|
||||
$swErr.WriteLine("--- stderr ---")
|
||||
foreach ($el in $errLines) {
|
||||
$swErr.WriteLine($el)
|
||||
}
|
||||
} finally {
|
||||
$swErr.Close()
|
||||
}
|
||||
foreach ($el in $errLines) {
|
||||
Write-PbrtLineToHost -Line $el -ShowProgress:$ShowProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $errTemp -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$sw.Stop()
|
||||
if (-not $ShowProgress -and $nProgress -gt 0) {
|
||||
Write-Host (" ({0} progress lines omitted here; see log file)" -f $nProgress) -ForegroundColor DarkGray
|
||||
}
|
||||
return @{
|
||||
ExitCode = $exitCode
|
||||
ProcessSeconds = $sw.Elapsed.TotalSeconds
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SsimCompareResult {
|
||||
param(
|
||||
[string] $PathA,
|
||||
[string] $PathB,
|
||||
[string] $RepoRoot
|
||||
)
|
||||
$py = Join-Path $RepoRoot "compare_ssim.py"
|
||||
if (-not (Test-Path -LiteralPath $py)) {
|
||||
return @{ Ok = $false; Value = $null; Message = "compare_ssim.py not found next to compare-skipmip.ps1" }
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $PathA) -or -not (Test-Path -LiteralPath $PathB)) {
|
||||
return @{ Ok = $false; Value = $null; Message = "one or both output images are missing" }
|
||||
}
|
||||
$usePy = $null
|
||||
if (Get-Command python -ErrorAction SilentlyContinue) { $usePy = "python" }
|
||||
elseif (Get-Command py -ErrorAction SilentlyContinue) { $usePy = "py" }
|
||||
else {
|
||||
return @{ Ok = $false; Value = $null; Message = "python not on PATH (install: pip install numpy scikit-image pillow)" }
|
||||
}
|
||||
try {
|
||||
$prevEap = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
if ($usePy -eq "py") {
|
||||
$outLines = & py -3 $py $PathA $PathB 2>&1
|
||||
} else {
|
||||
$outLines = & python $py $PathA $PathB 2>&1
|
||||
}
|
||||
$ErrorActionPreference = $prevEap
|
||||
$code = $LASTEXITCODE
|
||||
if ($code -ne 0) {
|
||||
$msg = ($outLines | ForEach-Object { "$_" }) -join " "
|
||||
return @{ Ok = $false; Value = $null; Message = $msg.Trim() }
|
||||
}
|
||||
$last = $outLines | Select-Object -Last 1
|
||||
$lastStr = "$last".Trim()
|
||||
[double]$v = 0.0
|
||||
$parsed = [double]::TryParse(
|
||||
$lastStr,
|
||||
[System.Globalization.NumberStyles]::Any,
|
||||
[System.Globalization.CultureInfo]::InvariantCulture,
|
||||
[ref]$v)
|
||||
if (-not $parsed) {
|
||||
return @{ Ok = $false; Value = $null; Message = "could not parse SSIM value: $lastStr" }
|
||||
}
|
||||
return @{ Ok = $true; Value = $v; Message = "" }
|
||||
} catch {
|
||||
return @{ Ok = $false; Value = $null; Message = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
# --- main ---
|
||||
|
||||
$PbrtExe = Resolve-PbrtExe -Explicit $PbrtExe
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Scene)) {
|
||||
throw "Scene file not found: $Scene"
|
||||
}
|
||||
$sceneFull = (Resolve-Path -LiteralPath $Scene).Path
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($LogDir)) {
|
||||
$LogDir = Join-Path $PSScriptRoot "compare-skipmip-logs"
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
$LogDir = (Resolve-Path -LiteralPath $LogDir).Path
|
||||
|
||||
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$base = [System.IO.Path]::GetFileNameWithoutExtension($sceneFull)
|
||||
|
||||
$imgNoMip = Join-Path $LogDir ("{0}-{1}-nomip.png" -f $base, $stamp)
|
||||
$imgSkipMip = Join-Path $LogDir ("{0}-{1}-skipmip.png" -f $base, $stamp)
|
||||
$logNoMip = Join-Path $LogDir ("{0}-{1}-nomip.txt" -f $base, $stamp)
|
||||
$logSkipMip = Join-Path $LogDir ("{0}-{1}-skipmip.txt" -f $base, $stamp)
|
||||
$summaryPath = Join-Path $LogDir ("{0}-{1}-comparison.txt" -f $base, $stamp)
|
||||
|
||||
$sppForCli = $null
|
||||
$sppFromNamed = $false
|
||||
if ($PSBoundParameters.ContainsKey('Spp')) {
|
||||
$sppFromNamed = $true
|
||||
if ($null -eq $Spp -or "$Spp" -eq '') {
|
||||
throw "Spp was specified but is empty; omit -Spp to use the scene file default."
|
||||
}
|
||||
try {
|
||||
$sppForCli = [int]$Spp
|
||||
} catch {
|
||||
throw "Invalid Spp value (expected positive integer): $Spp"
|
||||
}
|
||||
if ($sppForCli -lt 1) {
|
||||
throw "Spp must be >= 1 when specified (omit -Spp to use the scene file default)."
|
||||
}
|
||||
}
|
||||
|
||||
$tailPbrt = New-Object System.Collections.Generic.List[string]
|
||||
if ($RemainingArguments) {
|
||||
foreach ($a in $RemainingArguments) {
|
||||
$tailPbrt.Add($a)
|
||||
}
|
||||
}
|
||||
if (-not $sppFromNamed -and $tailPbrt.Count -gt 0) {
|
||||
$head = $tailPbrt[0]
|
||||
if ($head -match '^\d+$') {
|
||||
$trySpp = [int]$head
|
||||
if ($trySpp -ge 1) {
|
||||
$sppForCli = $trySpp
|
||||
$tailPbrt.RemoveAt(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$common = @(
|
||||
$sceneFull,
|
||||
"--stats"
|
||||
)
|
||||
if ($null -ne $sppForCli) {
|
||||
$common += @("--spp", "$sppForCli")
|
||||
}
|
||||
$common += $ExtraPbrtArgs
|
||||
|
||||
$gpuWanted = [bool]$Gpu
|
||||
$finalTail = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($a in $tailPbrt) {
|
||||
if ($a -eq '--gpu') {
|
||||
$gpuWanted = $true
|
||||
} else {
|
||||
$finalTail.Add($a)
|
||||
}
|
||||
}
|
||||
$tailPbrt = $finalTail
|
||||
if ($gpuWanted -and ($common -notcontains '--gpu')) {
|
||||
$common += '--gpu'
|
||||
}
|
||||
if ($tailPbrt.Count -gt 0) {
|
||||
$common += [string[]]$tailPbrt.ToArray()
|
||||
}
|
||||
|
||||
$sppLabel = if ($null -ne $sppForCli) { "$sppForCli" } else { "(scene file default)" }
|
||||
|
||||
Write-Host "=== pbrt compare-skipmip ===" -ForegroundColor Cyan
|
||||
Write-Host "pbrt: $PbrtExe"
|
||||
Write-Host "scene: $sceneFull"
|
||||
Write-Host "spp: $sppLabel"
|
||||
Write-Host "log dir: $LogDir"
|
||||
if ($gpuWanted) {
|
||||
Write-Host "gpu: --gpu (both runs)" -ForegroundColor DarkGray
|
||||
}
|
||||
if (-not $ShowProgress) {
|
||||
Write-Host "(progress lines hidden; use -ShowProgress to print every Rendering: line)" -ForegroundColor DarkGray
|
||||
}
|
||||
if ($VerboseMipPreprocess) {
|
||||
Write-Host "(SkipMip run: --verbose-mip-preprocess enabled)" -ForegroundColor DarkGray
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "================================================================" -ForegroundColor Yellow
|
||||
Write-Host " Run 1/2 | Full Mipmap Chain" -ForegroundColor Yellow
|
||||
Write-Host "================================================================" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
$argsNoMip = $common + @("--outfile", $imgNoMip)
|
||||
$runNoMip = Invoke-PbrtLogged -PbrtExe $PbrtExe -Arguments $argsNoMip -LogPath $logNoMip -ShowProgress:$ShowProgress
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================" -ForegroundColor Yellow
|
||||
Write-Host " Run 2/2 | SkipMip" -ForegroundColor Yellow
|
||||
Write-Host "================================================================" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
$argsSkipMip = $common + @("--skipmip", "--outfile", $imgSkipMip)
|
||||
if ($VerboseMipPreprocess) {
|
||||
$argsSkipMip += "--verbose-mip-preprocess"
|
||||
}
|
||||
$runSkipMip = Invoke-PbrtLogged -PbrtExe $PbrtExe -Arguments $argsSkipMip -LogPath $logSkipMip -ShowProgress:$ShowProgress
|
||||
|
||||
$textNo = Read-LogText $logNoMip
|
||||
$textSkip = Read-LogText $logSkipMip
|
||||
|
||||
# Stats block (from pbrt --stats): flexible spacing
|
||||
$patRss = 'RSS\s*\(current\)\s+([\d.]+\s+(?:kB|MiB|GiB))'
|
||||
$patWall = 'Wall-clock render time\s+([\d.]+)\s*s'
|
||||
$patMipWall = '\[mip preprocess\] wall time\s+([\d.]+)\s*s'
|
||||
$patImgTotal = 'Total \(counters\)\s+([\d.]+\s+(?:kB|MiB|GiB))'
|
||||
|
||||
$rssNo = Match-One -Text $textNo -Pattern $patRss
|
||||
$rssSkip = Match-One -Text $textSkip -Pattern $patRss
|
||||
$wallNo = Match-One -Text $textNo -Pattern $patWall
|
||||
$wallSkip = Match-One -Text $textSkip -Pattern $patWall
|
||||
$imgTotNo = Match-One -Text $textNo -Pattern $patImgTotal
|
||||
$imgTotSkip = Match-One -Text $textSkip -Pattern $patImgTotal
|
||||
$mipPreSkip = Match-One -Text $textSkip -Pattern $patMipWall
|
||||
|
||||
$rssNoB = Convert-MemoryToBytes $rssNo
|
||||
$rssSkipB = Convert-MemoryToBytes $rssSkip
|
||||
$imgNoB = Convert-MemoryToBytes $imgTotNo
|
||||
$imgSkipB = Convert-MemoryToBytes $imgTotSkip
|
||||
|
||||
$wallNoD = if ($wallNo) { [double]$wallNo } else { $null }
|
||||
$wallSkipD = if ($wallSkip) { [double]$wallSkip } else { $null }
|
||||
$mipPreD = if ($mipPreSkip) { [double]$mipPreSkip } else { $null }
|
||||
|
||||
$exitNo = [int]$runNoMip.ExitCode
|
||||
$exitSk = [int]$runSkipMip.ExitCode
|
||||
|
||||
$ssimRes = Get-SsimCompareResult -PathA $imgNoMip -PathB $imgSkipMip -RepoRoot $PSScriptRoot
|
||||
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
[void]$sb.AppendLine("================================")
|
||||
[void]$sb.AppendLine("PBRT SkipMip Comparison Report")
|
||||
[void]$sb.AppendLine("================================")
|
||||
[void]$sb.AppendLine("")
|
||||
[void]$sb.AppendLine("Scene : $sceneFull")
|
||||
[void]$sb.AppendLine("SPP : $sppLabel")
|
||||
[void]$sb.AppendLine("GPU : $(if ($gpuWanted) { '--gpu (yes)' } else { '(no)' })")
|
||||
[void]$sb.AppendLine("Stamp : $stamp")
|
||||
[void]$sb.AppendLine("")
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
[void]$sb.AppendLine("Outputs")
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
[void]$sb.AppendLine("Logs:")
|
||||
[void]$sb.AppendLine(" Full Mip : $logNoMip")
|
||||
[void]$sb.AppendLine(" SkipMip : $logSkipMip")
|
||||
[void]$sb.AppendLine("")
|
||||
[void]$sb.AppendLine("Images:")
|
||||
[void]$sb.AppendLine(" Full Mip : $imgNoMip")
|
||||
[void]$sb.AppendLine(" SkipMip : $imgSkipMip")
|
||||
[void]$sb.AppendLine("")
|
||||
if ($exitNo -ne 0 -or $exitSk -ne 0) {
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
[void]$sb.AppendLine("Exit Codes")
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
[void]$sb.AppendLine(" Full Mip : $exitNo")
|
||||
[void]$sb.AppendLine(" SkipMip : $exitSk")
|
||||
[void]$sb.AppendLine("")
|
||||
}
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
[void]$sb.AppendLine("Timing")
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
[void]$sb.AppendLine("Entire pbrt.exe Run-Time:")
|
||||
[void]$sb.AppendLine(" Full Mip : {0:N3}s" -f $runNoMip.ProcessSeconds)
|
||||
$skipProcessLine = " SkipMip : {0:N3}s" -f $runSkipMip.ProcessSeconds
|
||||
if ($null -ne $mipPreD) {
|
||||
$skipProcessLine += " (Mipmap Preprocess Time: {0:N3} s)" -f $mipPreD
|
||||
}
|
||||
[void]$sb.AppendLine($skipProcessLine)
|
||||
[void]$sb.AppendLine("")
|
||||
if ($null -ne $wallNoD -and $null -ne $wallSkipD) {
|
||||
[void]$sb.AppendLine("Render Time:")
|
||||
[void]$sb.AppendLine(" Full Mip : {0:N3} s" -f $wallNoD)
|
||||
[void]$sb.AppendLine(" SkipMip : {0:N3} s" -f $wallSkipD)
|
||||
[void]$sb.AppendLine(" Delta : {0:N3} s" -f ($wallSkipD - $wallNoD))
|
||||
[void]$sb.AppendLine("")
|
||||
}
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
[void]$sb.AppendLine("Memory")
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
[void]$sb.AppendLine("RSS")
|
||||
[void]$sb.AppendLine(" Full Mip : $(if ($rssNo) { $rssNo } else { 'n/a' })")
|
||||
[void]$sb.AppendLine(" SkipMip : $(if ($rssSkip) { $rssSkip } else { 'n/a' })")
|
||||
if ($null -ne $rssNoB -and $null -ne $rssSkipB) {
|
||||
$pctRss = Format-SavingsPercent -FullMipBytes $rssNoB -SkipMipBytes $rssSkipB
|
||||
[void]$sb.AppendLine(" Savings : $pctRss")
|
||||
}
|
||||
[void]$sb.AppendLine("")
|
||||
[void]$sb.AppendLine("Image Textures")
|
||||
[void]$sb.AppendLine(" Full Mip : $(if ($imgTotNo) { $imgTotNo } else { 'n/a' })")
|
||||
[void]$sb.AppendLine(" SkipMip : $(if ($imgTotSkip) { $imgTotSkip } else { 'n/a' })")
|
||||
if ($null -ne $imgNoB -and $null -ne $imgSkipB) {
|
||||
$pctImg = Format-SavingsPercent -FullMipBytes $imgNoB -SkipMipBytes $imgSkipB
|
||||
[void]$sb.AppendLine(" Savings : $pctImg")
|
||||
}
|
||||
[void]$sb.AppendLine("")
|
||||
if ($ssimRes.Ok) {
|
||||
$ssimMark = if ($ssimRes.Value -gt 0.99) { [char]0x2713 } else { [char]0x2717 }
|
||||
$ssimValStr = ([double]$ssimRes.Value).ToString(
|
||||
"N6", [System.Globalization.CultureInfo]::InvariantCulture)
|
||||
# Avoid -f with multiple placeholders (can throw FormatError with some hosts/encodings).
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
[void]$sb.AppendLine("SSIM (Full Mip vs SkipMip): $ssimValStr $ssimMark")
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
|
||||
} else {
|
||||
$why = if ($ssimRes.Message) {
|
||||
($ssimRes.Message -replace "[\r\n]+", " ").Trim()
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
if ($why.Length -gt 160) { $why = $why.Substring(0, 157) + "..." }
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
[void]$sb.AppendLine("SSIM (Full Mip vs SkipMip): n/a - $why")
|
||||
[void]$sb.AppendLine("-------------------------------------------")
|
||||
|
||||
}
|
||||
|
||||
$summary = $sb.ToString()
|
||||
[System.IO.File]::WriteAllText($summaryPath, $summary, (New-Object System.Text.UTF8Encoding $false))
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host " Summary | written to:" -ForegroundColor Cyan
|
||||
Write-Host " $summaryPath" -ForegroundColor Cyan
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host $summary
|
||||
|
||||
if ($exitNo -ne 0 -or $exitSk -ne 0) {
|
||||
exit [Math]::Max([Math]::Max($exitNo, $exitSk), 1)
|
||||
}
|
||||
exit 0
|
||||
56
compare_ssim.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX: same as pbrt-v4 / Apache-2.0 where applicable
|
||||
"""Print one SSIM value (0-1) for two images to stdout. Used by compare-skipmip.ps1.
|
||||
|
||||
Requires (same resolution, full image, RGB or grayscale as in skimage):
|
||||
pip install numpy scikit-image pillow
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: compare_ssim.py <image_a> <image_b>", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from skimage.metrics import structural_similarity as ssim
|
||||
except ImportError as e:
|
||||
print(
|
||||
"ImportError: %s\nInstall: pip install numpy scikit-image pillow" % e,
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
paths = sys.argv[1:3]
|
||||
for p in paths:
|
||||
if not Path(p).is_file():
|
||||
print("not found: %s" % p, file=sys.stderr)
|
||||
return 3
|
||||
|
||||
def load(path: str) -> "np.ndarray":
|
||||
im = Image.open(path)
|
||||
if im.mode not in ("RGB", "L"):
|
||||
im = im.convert("RGB")
|
||||
return np.asarray(im)
|
||||
|
||||
a, b = load(paths[0]), load(paths[1])
|
||||
if a.shape != b.shape:
|
||||
print("shape mismatch: %s vs %s" % (a.shape, b.shape), file=sys.stderr)
|
||||
return 4
|
||||
if a.ndim == 2:
|
||||
val = float(ssim(a, b, data_range=255))
|
||||
else:
|
||||
try:
|
||||
val = float(ssim(a, b, channel_axis=2, data_range=255))
|
||||
except TypeError:
|
||||
val = float(ssim(a, b, multichannel=True, data_range=255))
|
||||
print("%.6f" % val)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
BIN
scenes/book-full.png
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
BIN
scenes/book-skipmip.png
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
BIN
scenes/book.png
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
BIN
scenes/quick_preview.png
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
BIN
scenes/sanmiguel-full.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
scenes/sanmiguel-skipmip.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
scenes/sanmiguel.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
|
|
@ -272,6 +272,11 @@ class ProjectiveCamera : public CameraBase {
|
|||
cameraFromRaster = Inverse(screenFromCamera) * screenFromRaster;
|
||||
}
|
||||
|
||||
// Homogeneous camera-space point to raster (before perspective divide on x/y). Used by
|
||||
// texture mip preprocess for analytic primary visibility UV differentials.
|
||||
PBRT_CPU_GPU
|
||||
Transform GetRasterFromCameraTransform() const { return rasterFromScreen * screenFromCamera; }
|
||||
|
||||
protected:
|
||||
// ProjectiveCamera Protected Members
|
||||
Transform screenFromCamera, cameraFromRaster;
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@
|
|||
#include <pbrt/util/parallel.h>
|
||||
#include <pbrt/util/print.h>
|
||||
#include <pbrt/util/spectrum.h>
|
||||
#include <pbrt/util/stats.h>
|
||||
#include <pbrt/util/string.h>
|
||||
#include <pbrt/wavefront/wavefront.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -40,6 +42,11 @@ Rendering options:
|
|||
faster debugging. (<values> are Integrator-specific
|
||||
and come from error message text.)
|
||||
--disable-image-textures Always return the average value of image textures.
|
||||
--skipmip Skip the first k mip levels of image textures (box-filter
|
||||
downsample k times before building the mip pyramid). k defaults
|
||||
in mipmap.cpp or is chosen per texture by preprocess.
|
||||
--verbose-mip-preprocess With --skipmip, print per-texture/per-geometry mip analysis.
|
||||
Default is a single summary line (large logs are costly).
|
||||
--disable-pixel-jitter Always sample pixels at their centers.
|
||||
--disable-texture-filtering Point-sample all textures.
|
||||
--disable-wavelength-jitter Always sample the same %d wavelengths of light.
|
||||
|
|
@ -73,7 +80,7 @@ Rendering options:
|
|||
--render-coord-sys <name> Coordinate system to use for the scene when rendering,
|
||||
where name is "camera", "cameraworld", or "world".
|
||||
--seed <n> Set random number generator seed. Default: 0.
|
||||
--stats Print various statistics after rendering completes.
|
||||
--stats Print memory usage and render timing summaries after rendering.
|
||||
--spp <n> Override number of pixel samples specified in scene
|
||||
description file.
|
||||
--wavefront Use wavefront volumetric path integrator.
|
||||
|
|
@ -164,6 +171,9 @@ int main(int argc, char *argv[]) {
|
|||
ParseArg(&iter, args.end(), "debugstart", &options.debugStart, onError) ||
|
||||
ParseArg(&iter, args.end(), "disable-image-textures",
|
||||
&options.disableImageTextures, onError) ||
|
||||
ParseArg(&iter, args.end(), "skipmip", &options.skipMipImageTextures, onError) ||
|
||||
ParseArg(&iter, args.end(), "verbose-mip-preprocess",
|
||||
&options.verboseMipPreprocess, onError) ||
|
||||
ParseArg(&iter, args.end(), "disable-pixel-jitter",
|
||||
&options.disablePixelJitter, onError) ||
|
||||
ParseArg(&iter, args.end(), "disable-texture-filtering",
|
||||
|
|
@ -281,11 +291,22 @@ int main(int argc, char *argv[]) {
|
|||
BasicSceneBuilder builder(&scene);
|
||||
ParseFiles(&builder, filenames);
|
||||
|
||||
// Render the scene
|
||||
if (Options->useGPU || Options->wavefront)
|
||||
RenderWavefront(scene);
|
||||
else
|
||||
RenderCPU(scene);
|
||||
// Render the scene (includes scene setup inside RenderCPU / RenderWavefront: media,
|
||||
// camera, mip preprocess when --skipmip, textures, aggregate, integrator::Render, …).
|
||||
if (options.printStatistics) {
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
if (Options->useGPU || Options->wavefront)
|
||||
RenderWavefront(scene);
|
||||
else
|
||||
RenderCPU(scene);
|
||||
auto t1 = std::chrono::steady_clock::now();
|
||||
SetStatsRenderWallSeconds(std::chrono::duration<Float>(t1 - t0).count());
|
||||
} else {
|
||||
if (Options->useGPU || Options->wavefront)
|
||||
RenderWavefront(scene);
|
||||
else
|
||||
RenderCPU(scene);
|
||||
}
|
||||
|
||||
LOG_VERBOSE("Memory used after post-render cleanup: %s", GetCurrentRSS());
|
||||
// Clean up after rendering the scene
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <pbrt/samplers.h>
|
||||
#include <pbrt/scene.h>
|
||||
#include <pbrt/shapes.h>
|
||||
#include <pbrt/texture_mip_preprocess.h>
|
||||
#include <pbrt/textures.h>
|
||||
#include <pbrt/util/colorspace.h>
|
||||
#include <pbrt/util/parallel.h>
|
||||
|
|
@ -28,6 +29,12 @@ void RenderCPU(BasicScene &parsedScene) {
|
|||
// Create media first (so have them for the camera...)
|
||||
std::map<std::string, Medium> media = parsedScene.CreateMedia();
|
||||
|
||||
Camera camera = parsedScene.GetCamera();
|
||||
Sampler sampler = parsedScene.GetSampler();
|
||||
|
||||
LOG_VERBOSE("Image texture mip preprocess");
|
||||
RunImageTextureMipPreprocess(parsedScene, camera);
|
||||
|
||||
// Textures
|
||||
LOG_VERBOSE("Starting textures");
|
||||
NamedTextures textures = parsedScene.CreateTextures();
|
||||
|
|
@ -47,9 +54,7 @@ void RenderCPU(BasicScene &parsedScene) {
|
|||
Primitive accel = parsedScene.CreateAggregate(textures, shapeIndexToAreaLights, media,
|
||||
namedMaterials, materials);
|
||||
|
||||
Camera camera = parsedScene.GetCamera();
|
||||
Film film = camera.GetFilm();
|
||||
Sampler sampler = parsedScene.GetSampler();
|
||||
|
||||
// Integrator
|
||||
LOG_VERBOSE("Starting to create integrator");
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ struct __align__(OPTIX_SBT_RECORD_ALIGNMENT) OptiXAggregate::HitgroupRecord {
|
|||
|
||||
extern "C" {
|
||||
extern const unsigned char PBRT_EMBEDDED_PTX[];
|
||||
extern const size_t PBRT_EMBEDDED_PTX_SIZE;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
|
|
@ -1064,7 +1065,8 @@ OptixPipelineCompileOptions OptiXAggregate::getPipelineCompileOptions() {
|
|||
}
|
||||
|
||||
OptixModule OptiXAggregate::createOptiXModule(OptixDeviceContext optixContext,
|
||||
const char *ptx) {
|
||||
const char *moduleInput,
|
||||
size_t moduleInputSize) {
|
||||
OptixModuleCompileOptions moduleCompileOptions = {};
|
||||
// TODO: REVIEW THIS
|
||||
moduleCompileOptions.maxRegisterCount = OPTIX_COMPILE_DEFAULT_MAX_REGISTER_COUNT;
|
||||
|
|
@ -1099,7 +1101,7 @@ OptixModule OptiXAggregate::createOptiXModule(OptixDeviceContext optixContext,
|
|||
OPTIX_CHECK_WITH_LOG(
|
||||
OPTIX_MODULE_CREATE_FN(
|
||||
optixContext, &moduleCompileOptions, &pipelineCompileOptions,
|
||||
ptx, strlen(ptx), log, &logSize, &optixModule
|
||||
moduleInput, moduleInputSize, log, &logSize, &optixModule
|
||||
),
|
||||
log
|
||||
);
|
||||
|
|
@ -1234,7 +1236,8 @@ OptiXAggregate::OptiXAggregate(
|
|||
(OPTIX_VERSION % 10000) / 100, OPTIX_VERSION % 100);
|
||||
|
||||
// OptiX module
|
||||
optixModule = createOptiXModule(optixContext, (const char *)PBRT_EMBEDDED_PTX);
|
||||
optixModule = createOptiXModule(optixContext, (const char *)PBRT_EMBEDDED_PTX,
|
||||
PBRT_EMBEDDED_PTX_SIZE);
|
||||
|
||||
// Optix program groups...
|
||||
char log[4096];
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class OptiXAggregate : public WavefrontAggregate {
|
|||
int addHGRecords(const BVH &bvh);
|
||||
|
||||
static OptixModule createOptiXModule(OptixDeviceContext optixContext,
|
||||
const char *ptx);
|
||||
const char *moduleInput, size_t moduleInputSize);
|
||||
static OptixPipelineCompileOptions getPipelineCompileOptions();
|
||||
|
||||
OptixProgramGroup createRaygenPG(const char *entrypoint) const;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ std::string PBRTOptions::ToString() const {
|
|||
return StringPrintf(
|
||||
"[ PBRTOptions seed: %s quiet: %s disablePixelJitter: %s "
|
||||
"disableWavelengthJitter: %s disableTextureFiltering: %s disableImageTextures: %s "
|
||||
"skipMipImageTextures: %s verboseMipPreprocess: %s "
|
||||
"forceDiffuse: %s useGPU: %s wavefront: %s interactive: %s fullscreen %s "
|
||||
"renderingSpace: %s nThreads: %s logLevel: %s logFile: %s logUtilization: %s "
|
||||
"writePartialImages: %s recordPixelStatistics: %s "
|
||||
|
|
@ -44,7 +45,9 @@ std::string PBRTOptions::ToString() const {
|
|||
"displayServer: %s cropWindow: %s pixelBounds: %s pixelMaterial: %s "
|
||||
"displacementEdgeScale: %f ]",
|
||||
seed, quiet, disablePixelJitter, disableWavelengthJitter, disableTextureFiltering,
|
||||
disableImageTextures, forceDiffuse, useGPU, wavefront, interactive, fullscreen,
|
||||
disableImageTextures, skipMipImageTextures, verboseMipPreprocess, forceDiffuse, useGPU,
|
||||
wavefront,
|
||||
interactive, fullscreen,
|
||||
renderingSpace, nThreads, logLevel, logFile, logUtilization, writePartialImages,
|
||||
recordPixelStatistics, printStatistics, pixelSamples, gpuDevice, quickRender, upgrade,
|
||||
imageFile, mseReferenceImage, mseReferenceOutput, debugStart, displayServer, cropWindow,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ struct BasicPBRTOptions {
|
|||
bool disablePixelJitter = false, disableWavelengthJitter = false;
|
||||
bool disableTextureFiltering = false;
|
||||
bool disableImageTextures = false;
|
||||
// When true (--skipmip), load textures with the first k mips skipped (see
|
||||
// kDefaultImageTextureSkipMipLevelsWhenSkipMipEnabled in mipmap.cpp unless overridden
|
||||
// per file by RunImageTextureMipPreprocess). When false, full-res base.
|
||||
bool skipMipImageTextures = false;
|
||||
// With --skipmip: log each texture and geometry use for mip analysis (can be very slow).
|
||||
// Default false: one summary line with texture count and wall time.
|
||||
bool verboseMipPreprocess = false;
|
||||
bool forceDiffuse = false;
|
||||
bool useGPU = false;
|
||||
bool wavefront = false;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
|
||||
namespace pbrt {
|
||||
|
||||
|
|
@ -941,17 +942,8 @@ void BasicScene::AddFloatTexture(std::string name, TextureSceneEntity texture) {
|
|||
}
|
||||
loadingTextureFilenames.insert(filename);
|
||||
|
||||
auto create = [=](TextureSceneEntity texture) {
|
||||
Allocator alloc = threadAllocators.Get();
|
||||
|
||||
pbrt::Transform renderFromTexture = texture.renderFromObject.startTransform;
|
||||
// Pass nullptr for the textures, since they shouldn't be accessed
|
||||
// anyway.
|
||||
TextureParameterDictionary texDict(&texture.parameters, nullptr);
|
||||
return FloatTexture::Create(texture.name, renderFromTexture, texDict,
|
||||
&texture.loc, alloc, Options->useGPU);
|
||||
};
|
||||
floatTextureJobs[name] = RunAsync(create, texture);
|
||||
deferredFloatImageTextureJobs.push_back(
|
||||
std::make_pair(std::move(name), std::move(texture)));
|
||||
}
|
||||
|
||||
void BasicScene::AddSpectrumTexture(std::string name, TextureSceneEntity texture) {
|
||||
|
|
@ -984,20 +976,8 @@ void BasicScene::AddSpectrumTexture(std::string name, TextureSceneEntity texture
|
|||
loadingTextureFilenames.insert(filename);
|
||||
|
||||
asyncSpectrumTextures.push_back(std::make_pair(name, texture));
|
||||
|
||||
auto create = [=](TextureSceneEntity texture) {
|
||||
Allocator alloc = threadAllocators.Get();
|
||||
|
||||
pbrt::Transform renderFromTexture = texture.renderFromObject.startTransform;
|
||||
// nullptr for the textures, as with float textures.
|
||||
TextureParameterDictionary texDict(&texture.parameters, nullptr);
|
||||
// Only create SpectrumType::Albedo for now; will get the other two
|
||||
// types in CreateTextures().
|
||||
return SpectrumTexture::Create(texture.name, renderFromTexture, texDict,
|
||||
SpectrumType::Albedo, &texture.loc, alloc,
|
||||
Options->useGPU);
|
||||
};
|
||||
spectrumTextureJobs[name] = RunAsync(create, texture);
|
||||
deferredSpectrumImageTextureJobs.push_back(
|
||||
std::make_pair(std::move(name), std::move(texture)));
|
||||
}
|
||||
|
||||
void BasicScene::AddLight(LightSceneEntity light) {
|
||||
|
|
@ -1170,9 +1150,129 @@ void BasicScene::CreateMaterials(const NamedTextures &textures,
|
|||
}
|
||||
}
|
||||
|
||||
bool BasicScene::LookupShapeMaterial(const ShapeSceneEntity &shape, SceneEntity *mtlOut) const {
|
||||
if (!shape.materialName.empty()) {
|
||||
for (const auto &nm : namedMaterials)
|
||||
if (nm.first == shape.materialName) {
|
||||
*mtlOut = nm.second;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (shape.materialIndex < 0 || size_t(shape.materialIndex) >= materials.size())
|
||||
return false;
|
||||
*mtlOut = materials[shape.materialIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BasicScene::LookupNamedMaterial(const std::string &name, SceneEntity *mtlOut) const {
|
||||
for (const auto &nm : namedMaterials)
|
||||
if (nm.first == name) {
|
||||
*mtlOut = nm.second;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::map<std::string, SpectrumImagemapDeclarationInfo> BasicScene::SpectrumImagemapDeclarations()
|
||||
const {
|
||||
std::map<std::string, SpectrumImagemapDeclarationInfo> out;
|
||||
auto add = [&](const std::pair<std::string, TextureSceneEntity> &p) {
|
||||
if (p.second.name != "imagemap")
|
||||
return;
|
||||
std::string fn = ResolveFilename(p.second.parameters.GetOneString("filename", ""));
|
||||
if (fn.empty())
|
||||
return;
|
||||
SpectrumImagemapDeclarationInfo info;
|
||||
info.resolvedFilename = std::move(fn);
|
||||
info.su = p.second.parameters.GetOneFloat("uscale", 1.f);
|
||||
info.sv = p.second.parameters.GetOneFloat("vscale", 1.f);
|
||||
info.du = p.second.parameters.GetOneFloat("udelta", 0.f);
|
||||
info.dv = p.second.parameters.GetOneFloat("vdelta", 0.f);
|
||||
info.maxAnisotropy = p.second.parameters.GetOneFloat("maxanisotropy", 8.f);
|
||||
info.filter = p.second.parameters.GetOneString("filter", "bilinear");
|
||||
out[p.first] = std::move(info);
|
||||
};
|
||||
|
||||
std::lock_guard<std::mutex> lock(textureMutex);
|
||||
for (const auto &p : serialFloatTextures)
|
||||
add(p);
|
||||
for (const auto &p : serialSpectrumTextures)
|
||||
add(p);
|
||||
for (const auto &p : asyncSpectrumTextures)
|
||||
add(p);
|
||||
for (const auto &p : deferredFloatImageTextureJobs)
|
||||
add(p);
|
||||
for (const auto &p : deferredSpectrumImageTextureJobs)
|
||||
add(p);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::string> BasicScene::CollectResolvedImageTextureFilenames() {
|
||||
std::set<std::string> paths;
|
||||
auto addImagemapFilename = [&](const TextureSceneEntity &t) {
|
||||
if (t.name != "imagemap")
|
||||
return;
|
||||
std::string fn = ResolveFilename(t.parameters.GetOneString("filename", ""));
|
||||
if (!fn.empty())
|
||||
paths.insert(fn);
|
||||
};
|
||||
|
||||
std::lock_guard<std::mutex> lock(textureMutex);
|
||||
for (const auto &fn : loadingTextureFilenames)
|
||||
paths.insert(fn);
|
||||
for (const auto &p : deferredFloatImageTextureJobs)
|
||||
addImagemapFilename(p.second);
|
||||
for (const auto &p : deferredSpectrumImageTextureJobs)
|
||||
addImagemapFilename(p.second);
|
||||
for (const auto &p : serialFloatTextures)
|
||||
addImagemapFilename(p.second);
|
||||
for (const auto &p : serialSpectrumTextures)
|
||||
addImagemapFilename(p.second);
|
||||
for (const auto &p : asyncSpectrumTextures)
|
||||
addImagemapFilename(p.second);
|
||||
|
||||
return std::vector<std::string>(paths.begin(), paths.end());
|
||||
}
|
||||
|
||||
void BasicScene::LaunchDeferredImageTextureJobs() {
|
||||
std::lock_guard<std::mutex> lock(textureMutex);
|
||||
|
||||
for (auto &p : deferredFloatImageTextureJobs) {
|
||||
const std::string &name = p.first;
|
||||
TextureSceneEntity texture = std::move(p.second);
|
||||
auto create = [=](TextureSceneEntity tex) {
|
||||
Allocator alloc = threadAllocators.Get();
|
||||
pbrt::Transform renderFromTexture = tex.renderFromObject.startTransform;
|
||||
TextureParameterDictionary texDict(&tex.parameters, nullptr);
|
||||
return FloatTexture::Create(tex.name, renderFromTexture, texDict, &tex.loc, alloc,
|
||||
Options->useGPU);
|
||||
};
|
||||
floatTextureJobs[name] = RunAsync(create, texture);
|
||||
}
|
||||
deferredFloatImageTextureJobs.clear();
|
||||
|
||||
for (auto &p : deferredSpectrumImageTextureJobs) {
|
||||
const std::string &name = p.first;
|
||||
TextureSceneEntity texture = std::move(p.second);
|
||||
auto create = [=](TextureSceneEntity tex) {
|
||||
Allocator alloc = threadAllocators.Get();
|
||||
pbrt::Transform renderFromTexture = tex.renderFromObject.startTransform;
|
||||
TextureParameterDictionary texDict(&tex.parameters, nullptr);
|
||||
return SpectrumTexture::Create(tex.name, renderFromTexture, texDict,
|
||||
SpectrumType::Albedo, &tex.loc, alloc,
|
||||
Options->useGPU);
|
||||
};
|
||||
spectrumTextureJobs[name] = RunAsync(create, texture);
|
||||
}
|
||||
deferredSpectrumImageTextureJobs.clear();
|
||||
}
|
||||
|
||||
NamedTextures BasicScene::CreateTextures() {
|
||||
NamedTextures textures;
|
||||
|
||||
LaunchDeferredImageTextureJobs();
|
||||
|
||||
if (nMissingTextures > 0)
|
||||
ErrorExit("%d missing textures", nMissingTextures);
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,14 @@ namespace pbrt {
|
|||
|
||||
class Integrator;
|
||||
|
||||
// Declared spectrum imagemap textures (name -> file + UV scale), for preprocess / analysis.
|
||||
struct SpectrumImagemapDeclarationInfo {
|
||||
std::string resolvedFilename;
|
||||
Float su = 1, sv = 1, du = 0, dv = 0;
|
||||
Float maxAnisotropy = 8.f;
|
||||
std::string filter = "bilinear";
|
||||
};
|
||||
|
||||
// SceneEntity Definition
|
||||
struct SceneEntity {
|
||||
// SceneEntity Public Methods
|
||||
|
|
@ -324,6 +332,18 @@ class BasicScene {
|
|||
Primitive accel,
|
||||
std::vector<Light> lights) const;
|
||||
|
||||
// Resolved paths for imagemap textures (including deferred async loads not yet started).
|
||||
std::vector<std::string> CollectResolvedImageTextureFilenames();
|
||||
|
||||
// Parameter names -> imagemap paths (spectrum + float declarations).
|
||||
std::map<std::string, SpectrumImagemapDeclarationInfo> SpectrumImagemapDeclarations() const;
|
||||
|
||||
// Resolve material for a world-block shape (named material or material index).
|
||||
bool LookupShapeMaterial(const ShapeSceneEntity &shape, SceneEntity *mtlOut) const;
|
||||
|
||||
// Named material from Attribute "material" / MakeNamedMaterial (for preprocess, etc.).
|
||||
bool LookupNamedMaterial(const std::string &name, SceneEntity *mtlOut) const;
|
||||
|
||||
NamedTextures CreateTextures();
|
||||
|
||||
// BasicScene Public Members
|
||||
|
|
@ -340,6 +360,8 @@ class BasicScene {
|
|||
|
||||
void startLoadingNormalMaps(const ParameterDictionary ¶meters);
|
||||
|
||||
void LaunchDeferredImageTextureJobs();
|
||||
|
||||
// BasicScene Private Members
|
||||
AsyncJob<Sampler> *samplerJob = nullptr;
|
||||
mutable ThreadLocal<Allocator> threadAllocators;
|
||||
|
|
@ -365,10 +387,12 @@ class BasicScene {
|
|||
std::mutex areaLightMutex;
|
||||
std::vector<SceneEntity> areaLights;
|
||||
|
||||
std::mutex textureMutex;
|
||||
mutable std::mutex textureMutex;
|
||||
std::vector<std::pair<std::string, TextureSceneEntity>> serialFloatTextures;
|
||||
std::vector<std::pair<std::string, TextureSceneEntity>> serialSpectrumTextures;
|
||||
std::vector<std::pair<std::string, TextureSceneEntity>> asyncSpectrumTextures;
|
||||
std::vector<std::pair<std::string, TextureSceneEntity>> deferredFloatImageTextureJobs;
|
||||
std::vector<std::pair<std::string, TextureSceneEntity>> deferredSpectrumImageTextureJobs;
|
||||
std::set<std::string> loadingTextureFilenames;
|
||||
std::map<std::string, AsyncJob<FloatTexture> *> floatTextureJobs;
|
||||
std::map<std::string, AsyncJob<SpectrumTexture> *> spectrumTextureJobs;
|
||||
|
|
|
|||
670
src/pbrt/texture_mip_preprocess.cpp
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
// pbrt is Copyright(c) 1998-2020 Matt Pharr, Wenzel Jakob, and Greg Humphreys.
|
||||
// The pbrt source code is licensed under the Apache License, Version 2.0.
|
||||
// SPDX: Apache-2.0
|
||||
|
||||
#include <pbrt/texture_mip_preprocess.h>
|
||||
|
||||
#include <pbrt/cameras.h>
|
||||
#include <pbrt/film.h>
|
||||
#include <pbrt/options.h>
|
||||
#include <pbrt/scene.h>
|
||||
#include <pbrt/textures.h>
|
||||
#include <pbrt/util/check.h>
|
||||
#include <pbrt/util/file.h>
|
||||
#include <pbrt/util/image.h>
|
||||
#include <pbrt/util/math.h>
|
||||
#include <pbrt/util/mesh.h>
|
||||
#include <pbrt/util/mipmap.h>
|
||||
#include <pbrt/util/parallel.h>
|
||||
#include <pbrt/util/print.h>
|
||||
#include <pbrt/util/progressreporter.h>
|
||||
#include <pbrt/util/spectrum.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace pbrt {
|
||||
|
||||
// Per-geometry mip analysis logging (--verbose-mip-preprocess). Default is quiet: one summary
|
||||
// line in RunImageTextureMipPreprocess (large Printf volume dominates wall time otherwise).
|
||||
static bool MipPreprocessLogDetail() {
|
||||
return Options && Options->verboseMipPreprocess;
|
||||
}
|
||||
|
||||
// Log path relative to a .../pbrt-v4-scenes/ root when present (any slash style, case fold).
|
||||
static std::string ShortScenePathForMipLog(const std::string &fullPath) {
|
||||
static constexpr char kMarker[] = "pbrt-v4-scenes";
|
||||
const size_t n = fullPath.size(), m = sizeof(kMarker) - 1;
|
||||
for (size_t i = 0; i + m <= n; ++i) {
|
||||
bool match = true;
|
||||
for (size_t j = 0; j < m; ++j) {
|
||||
if (std::tolower(static_cast<unsigned char>(fullPath[i + j])) !=
|
||||
std::tolower(static_cast<unsigned char>(kMarker[j]))) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
size_t after = i + m;
|
||||
while (after < n && (fullPath[after] == '/' || fullPath[after] == '\\'))
|
||||
++after;
|
||||
return fullPath.substr(after);
|
||||
}
|
||||
}
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
// Declarations vs. CollectResolvedImageTextureFilenames may differ only by slash style or case
|
||||
// on Windows; treat those as the same file.
|
||||
static bool ResolvedImageTexturePathsEqual(const std::string &a, const std::string &b) {
|
||||
if (a == b)
|
||||
return true;
|
||||
#ifdef PBRT_IS_WINDOWS
|
||||
std::string na = a, nb = b;
|
||||
for (char &c : na)
|
||||
if (c == '/')
|
||||
c = '\\';
|
||||
for (char &c : nb)
|
||||
if (c == '/')
|
||||
c = '\\';
|
||||
return _stricmp(na.c_str(), nb.c_str()) == 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Hash map key consistent with ResolvedImageTexturePathsEqual on Windows.
|
||||
static std::string PathLookupKey(const std::string &path) {
|
||||
#ifdef PBRT_IS_WINDOWS
|
||||
std::string key = path;
|
||||
for (char &c : key) {
|
||||
if (c == '/')
|
||||
c = '\\';
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return key;
|
||||
#else
|
||||
return path;
|
||||
#endif
|
||||
}
|
||||
|
||||
// First spelling in `files` order wins; used for O(1) canonical paths instead of scanning all files
|
||||
// per imagemap use and per texture in RunImageTextureMipPreprocess.
|
||||
static std::unordered_map<std::string, std::string> BuildPathKeyToCanonicalMap(
|
||||
const std::vector<std::string> &files) {
|
||||
std::unordered_map<std::string, std::string> pathKeyToCanonical;
|
||||
pathKeyToCanonical.reserve(files.size() * 2);
|
||||
for (const std::string &fn : files) {
|
||||
std::string key = PathLookupKey(fn);
|
||||
if (pathKeyToCanonical.find(key) == pathKeyToCanonical.end())
|
||||
pathKeyToCanonical.emplace(std::move(key), fn);
|
||||
}
|
||||
return pathKeyToCanonical;
|
||||
}
|
||||
|
||||
static std::string CanonicalTexturePathForMip(const std::string &path,
|
||||
const std::unordered_map<std::string, std::string> &pathKeyToCanonical) {
|
||||
auto it = pathKeyToCanonical.find(PathLookupKey(path));
|
||||
if (it != pathKeyToCanonical.end())
|
||||
return it->second;
|
||||
return path;
|
||||
}
|
||||
|
||||
static Transform InstanceWorldFromObject(const InstanceSceneEntity &inst) {
|
||||
if (inst.renderFromInstanceAnim)
|
||||
return inst.renderFromInstanceAnim->Interpolate(0.5f);
|
||||
if (inst.renderFromInstance)
|
||||
return *inst.renderFromInstance;
|
||||
return Transform();
|
||||
}
|
||||
|
||||
static void ExtractTrianglesFromShape(const ShapeSceneEntity &sh,
|
||||
std::vector<ImageTextureMeshTriangle> *out) {
|
||||
std::vector<int> vi = sh.parameters.GetIntArray("indices");
|
||||
std::vector<Point3f> P = sh.parameters.GetPoint3fArray("P");
|
||||
std::vector<Point2f> uvs = sh.parameters.GetPoint2fArray("uv");
|
||||
if (P.empty())
|
||||
return;
|
||||
if (vi.empty() && P.size() == 3)
|
||||
vi = {0, 1, 2};
|
||||
if (vi.empty() || (vi.size() % 3) != 0)
|
||||
return;
|
||||
|
||||
const bool haveUvPerVertex = (uvs.size() == P.size());
|
||||
|
||||
for (size_t t = 0; t + 2 < vi.size(); t += 3) {
|
||||
int i0 = vi[t], i1 = vi[t + 1], i2 = vi[t + 2];
|
||||
if (i0 < 0 || i1 < 0 || i2 < 0 || size_t(i0) >= P.size() || size_t(i1) >= P.size() ||
|
||||
size_t(i2) >= P.size())
|
||||
continue;
|
||||
ImageTextureMeshTriangle tri;
|
||||
tri.p0 = P[i0];
|
||||
tri.p1 = P[i1];
|
||||
tri.p2 = P[i2];
|
||||
if (haveUvPerVertex) {
|
||||
tri.uv0 = uvs[i0];
|
||||
tri.uv1 = uvs[i1];
|
||||
tri.uv2 = uvs[i2];
|
||||
} else {
|
||||
tri.uv0 = Point2f(0, 0);
|
||||
tri.uv1 = Point2f(1, 0);
|
||||
tri.uv2 = Point2f(1, 1);
|
||||
}
|
||||
out->push_back(tri);
|
||||
}
|
||||
}
|
||||
|
||||
// Inline materials use SceneEntity::name; named materials store the plugin in "string type"
|
||||
// (see BasicScene::CreateMaterials).
|
||||
static std::string MaterialPluginType(const SceneEntity &mtl) {
|
||||
std::string t = mtl.parameters.GetOneString("type", "");
|
||||
if (!t.empty())
|
||||
return t;
|
||||
return std::string(mtl.name);
|
||||
}
|
||||
|
||||
// Spectrum imagemap names wired to "texture reflectance" on diffuse-like BxDFs, including via
|
||||
// "mix" of named materials (book scenes use this).
|
||||
static void CollectReflectanceSpectrumTextureNames(const BasicScene &scene, const SceneEntity &mtl,
|
||||
std::unordered_set<std::string> *mixVisit,
|
||||
std::vector<std::string> *out) {
|
||||
const std::string matType = MaterialPluginType(mtl);
|
||||
if (matType == "diffuse" || matType == "coateddiffuse" || matType == "diffusetransmission") {
|
||||
std::string ref = mtl.parameters.GetTexture("reflectance");
|
||||
if (!ref.empty())
|
||||
out->push_back(std::move(ref));
|
||||
return;
|
||||
}
|
||||
if (matType != "mix")
|
||||
return;
|
||||
|
||||
std::vector<std::string> subNames = mtl.parameters.GetStringArray("materials");
|
||||
if (subNames.size() != 2)
|
||||
return;
|
||||
for (const std::string &sub : subNames) {
|
||||
if (!mixVisit->insert(sub).second)
|
||||
continue;
|
||||
SceneEntity subEnt;
|
||||
if (scene.LookupNamedMaterial(sub, &subEnt))
|
||||
CollectReflectanceSpectrumTextureNames(scene, subEnt, mixVisit, out);
|
||||
mixVisit->erase(sub);
|
||||
}
|
||||
}
|
||||
|
||||
static void ExtractTrianglesFromPlyMeshShape(const ShapeSceneEntity &sh,
|
||||
std::unordered_map<std::string, TriQuadMesh> *plyCache,
|
||||
std::vector<ImageTextureMeshTriangle> *out) {
|
||||
std::string resolvedPly = ResolveFilename(sh.parameters.GetOneString("filename", ""));
|
||||
if (resolvedPly.empty())
|
||||
return;
|
||||
|
||||
auto it = plyCache->find(resolvedPly);
|
||||
if (it == plyCache->end()) {
|
||||
TriQuadMesh mesh = TriQuadMesh::ReadPLY(resolvedPly);
|
||||
mesh.ConvertToOnlyTriangles();
|
||||
it = plyCache->emplace(std::move(resolvedPly), std::move(mesh)).first;
|
||||
}
|
||||
const TriQuadMesh &mesh = it->second;
|
||||
if (mesh.triIndices.empty())
|
||||
return;
|
||||
|
||||
const bool haveUvPerVertex =
|
||||
!mesh.uv.empty() && mesh.uv.size() == mesh.p.size();
|
||||
|
||||
for (size_t t = 0; t + 2 < mesh.triIndices.size(); t += 3) {
|
||||
int i0 = mesh.triIndices[t], i1 = mesh.triIndices[t + 1], i2 = mesh.triIndices[t + 2];
|
||||
if (i0 < 0 || i1 < 0 || i2 < 0 || size_t(i0) >= mesh.p.size() ||
|
||||
size_t(i1) >= mesh.p.size() || size_t(i2) >= mesh.p.size())
|
||||
continue;
|
||||
ImageTextureMeshTriangle tri;
|
||||
tri.p0 = mesh.p[i0];
|
||||
tri.p1 = mesh.p[i1];
|
||||
tri.p2 = mesh.p[i2];
|
||||
if (haveUvPerVertex) {
|
||||
tri.uv0 = mesh.uv[i0];
|
||||
tri.uv1 = mesh.uv[i1];
|
||||
tri.uv2 = mesh.uv[i2];
|
||||
} else {
|
||||
tri.uv0 = Point2f(0, 0);
|
||||
tri.uv1 = Point2f(1, 0);
|
||||
tri.uv2 = Point2f(1, 1);
|
||||
}
|
||||
out->push_back(tri);
|
||||
}
|
||||
}
|
||||
|
||||
static void FillLocalMeshTrianglesForShape(const ShapeSceneEntity &sh,
|
||||
std::unordered_map<std::string, TriQuadMesh> *plyCache,
|
||||
std::vector<ImageTextureMeshTriangle> *tris) {
|
||||
if (sh.name == "trianglemesh")
|
||||
ExtractTrianglesFromShape(sh, tris);
|
||||
else if (sh.name == "plymesh")
|
||||
ExtractTrianglesFromPlyMeshShape(sh, plyCache, tris);
|
||||
}
|
||||
|
||||
static void AppendReflectanceImagemapUsesForMeshShape(
|
||||
const BasicScene &scene, const ShapeSceneEntity &sh, const Transform &worldFromShape,
|
||||
const std::string &geometryDebugLabel, const std::map<std::string, SpectrumImagemapDeclarationInfo> &decls,
|
||||
const std::unordered_map<std::string, std::string> &pathKeyToCanonical,
|
||||
const std::string *instanceMeshCacheKey,
|
||||
std::unordered_map<std::string, std::shared_ptr<const std::vector<ImageTextureMeshTriangle>>>
|
||||
*instanceMeshCache,
|
||||
std::unordered_map<std::string, std::vector<ImageTextureGeometryUse>> *usesByFile,
|
||||
std::unordered_map<std::string, TriQuadMesh> *plyCache) {
|
||||
if (sh.name != "trianglemesh" && sh.name != "plymesh")
|
||||
return;
|
||||
|
||||
SceneEntity mtl;
|
||||
if (!scene.LookupShapeMaterial(sh, &mtl))
|
||||
return;
|
||||
|
||||
std::vector<std::string> refTexNames;
|
||||
std::unordered_set<std::string> mixVisit;
|
||||
CollectReflectanceSpectrumTextureNames(scene, mtl, &mixVisit, &refTexNames);
|
||||
if (refTexNames.empty())
|
||||
return;
|
||||
|
||||
std::shared_ptr<const std::vector<ImageTextureMeshTriangle>> meshTris;
|
||||
if (instanceMeshCacheKey) {
|
||||
CHECK(instanceMeshCache != nullptr);
|
||||
auto it = instanceMeshCache->find(*instanceMeshCacheKey);
|
||||
if (it == instanceMeshCache->end()) {
|
||||
auto tris = std::make_shared<std::vector<ImageTextureMeshTriangle>>();
|
||||
FillLocalMeshTrianglesForShape(sh, plyCache, tris.get());
|
||||
meshTris = std::move(tris);
|
||||
(*instanceMeshCache)[*instanceMeshCacheKey] = meshTris;
|
||||
} else
|
||||
meshTris = it->second;
|
||||
} else {
|
||||
auto tris = std::make_shared<std::vector<ImageTextureMeshTriangle>>();
|
||||
FillLocalMeshTrianglesForShape(sh, plyCache, tris.get());
|
||||
meshTris = std::move(tris);
|
||||
}
|
||||
|
||||
if (!meshTris || meshTris->empty())
|
||||
return;
|
||||
|
||||
std::unordered_set<std::string> dedup;
|
||||
for (const std::string &refTex : refTexNames) {
|
||||
if (!dedup.insert(refTex).second)
|
||||
continue;
|
||||
|
||||
auto declIt = decls.find(refTex);
|
||||
if (declIt == decls.end())
|
||||
continue;
|
||||
const SpectrumImagemapDeclarationInfo &info = declIt->second;
|
||||
|
||||
pstd::optional<FilterFunction> ff = ParseFilter(info.filter);
|
||||
FilterFunction filter = ff.value_or(FilterFunction::Bilinear);
|
||||
|
||||
std::string fileKey = CanonicalTexturePathForMip(info.resolvedFilename, pathKeyToCanonical);
|
||||
|
||||
ImageTextureGeometryUse use;
|
||||
use.resolvedImageFilename = fileKey;
|
||||
use.geometryDebugLabel = geometryDebugLabel;
|
||||
use.localTriangles = meshTris;
|
||||
use.worldFromShape = worldFromShape;
|
||||
use.su = info.su;
|
||||
use.sv = info.sv;
|
||||
use.du = info.du;
|
||||
use.dv = info.dv;
|
||||
use.maxAnisotropy = info.maxAnisotropy;
|
||||
use.filter = filter;
|
||||
(*usesByFile)[fileKey].push_back(std::move(use));
|
||||
}
|
||||
}
|
||||
|
||||
// One scene pass: collect geometry uses per texture file (canonical keys from pathKeyToCanonical).
|
||||
static std::unordered_map<std::string, std::vector<ImageTextureGeometryUse>>
|
||||
GatherImageTextureUsesByFile(const BasicScene &scene,
|
||||
const std::map<std::string, SpectrumImagemapDeclarationInfo> &decls,
|
||||
const std::unordered_map<std::string, std::string> &pathKeyToCanonical,
|
||||
std::unordered_map<std::string, TriQuadMesh> *plyCache) {
|
||||
std::unordered_map<std::string, std::vector<ImageTextureGeometryUse>> usesByFile;
|
||||
std::unordered_map<std::string, std::shared_ptr<const std::vector<ImageTextureMeshTriangle>>>
|
||||
instanceMeshCache;
|
||||
|
||||
for (size_t si = 0; si < scene.shapes.size(); ++si) {
|
||||
const ShapeSceneEntity &sh = scene.shapes[si];
|
||||
Transform worldFromShape =
|
||||
sh.renderFromObject ? *sh.renderFromObject : Transform();
|
||||
AppendReflectanceImagemapUsesForMeshShape(
|
||||
scene, sh, worldFromShape,
|
||||
StringPrintf("shape[%zu]_%s", si, std::string(sh.name).c_str()), decls, pathKeyToCanonical,
|
||||
nullptr, nullptr, &usesByFile, plyCache);
|
||||
}
|
||||
|
||||
for (size_t ii = 0; ii < scene.instances.size(); ++ii) {
|
||||
const InstanceSceneEntity &inst = scene.instances[ii];
|
||||
auto defIt = scene.instanceDefinitions.find(inst.name);
|
||||
if (defIt == scene.instanceDefinitions.end() || defIt->second == nullptr)
|
||||
continue;
|
||||
const InstanceDefinitionSceneEntity &def = *defIt->second;
|
||||
Transform worldFromObject = InstanceWorldFromObject(inst);
|
||||
for (size_t si = 0; si < def.shapes.size(); ++si) {
|
||||
const ShapeSceneEntity &sh = def.shapes[si];
|
||||
Transform shapeLocalFromMesh =
|
||||
sh.renderFromObject ? *sh.renderFromObject : Transform();
|
||||
Transform worldFromShape = worldFromObject * shapeLocalFromMesh;
|
||||
std::string meshKey =
|
||||
StringPrintf("idef:%s:%zu", std::string(inst.name).c_str(), si);
|
||||
AppendReflectanceImagemapUsesForMeshShape(
|
||||
scene, sh, worldFromShape,
|
||||
StringPrintf("instance[%zu]_def_%s_shape[%zu]_%s", ii,
|
||||
std::string(inst.name).c_str(), si, std::string(sh.name).c_str()),
|
||||
decls, pathKeyToCanonical, &meshKey, &instanceMeshCache, &usesByFile, plyCache);
|
||||
}
|
||||
}
|
||||
|
||||
return usesByFile;
|
||||
}
|
||||
|
||||
static void MultiplyMatrixPointHomogeneous(const SquareMatrix<4> &m, Point3f p, Float *ox,
|
||||
Float *oy, Float *ow) {
|
||||
Float x = p.x, y = p.y, z = p.z;
|
||||
*ox = m[0][0] * x + m[0][1] * y + m[0][2] * z + m[0][3];
|
||||
*oy = m[1][0] * x + m[1][1] * y + m[1][2] * z + m[1][3];
|
||||
*ow = m[3][0] * x + m[3][1] * y + m[3][2] * z + m[3][3];
|
||||
}
|
||||
|
||||
// One 2×2 inverse for u/w, v/w, and 1/w on the same screen-space triangle.
|
||||
static bool AffineTripleScalarGradients2D(Float x0, Float y0, Float x1, Float y1, Float x2,
|
||||
Float y2, Float fU0, Float fU1, Float fU2, Float *gxU,
|
||||
Float *gyU, Float fV0, Float fV1, Float fV2, Float *gxV,
|
||||
Float *gyV, Float fI0, Float fI1, Float fI2, Float *gxI,
|
||||
Float *gyI) {
|
||||
Float s0x = x1 - x0, s0y = y1 - y0;
|
||||
Float s1x = x2 - x0, s1y = y2 - y0;
|
||||
Float det = s0x * s1y - s0y * s1x;
|
||||
if (std::abs(det) < 1e-20f)
|
||||
return false;
|
||||
Float inv00 = s1y / det, inv01 = -s0y / det, inv10 = -s1x / det, inv11 = s0x / det;
|
||||
auto grad = [&](Float a0, Float a1, Float a2, Float *gx, Float *gy) {
|
||||
Float d0 = a1 - a0, d1 = a2 - a0;
|
||||
*gx = d0 * inv00 + d1 * inv10;
|
||||
*gy = d0 * inv01 + d1 * inv11;
|
||||
};
|
||||
grad(fU0, fU1, fU2, gxU, gyU);
|
||||
grad(fV0, fV1, fV2, gxV, gyV);
|
||||
grad(fI0, fI1, fI2, gxI, gyI);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Float MinPrimaryContinuousLodForUse(const Camera &camera, const ImageTextureGeometryUse &use,
|
||||
int pyramidLevels, Allocator alloc) {
|
||||
(void)alloc;
|
||||
if (!Options || Options->disableTextureFiltering || !use.localTriangles ||
|
||||
use.localTriangles->empty())
|
||||
return 0;
|
||||
|
||||
const PerspectiveCamera *persp = camera.Cast<PerspectiveCamera>();
|
||||
const OrthographicCamera *ortho = camera.Cast<OrthographicCamera>();
|
||||
const ProjectiveCamera *proj = persp ? static_cast<const ProjectiveCamera *>(persp)
|
||||
: ortho ? static_cast<const ProjectiveCamera *>(ortho)
|
||||
: nullptr;
|
||||
if (!proj)
|
||||
return 0;
|
||||
|
||||
Transform rasterFromCamera = proj->GetRasterFromCameraTransform();
|
||||
const SquareMatrix<4> &M = rasterFromCamera.GetMatrix();
|
||||
Transform cameraFromRender = proj->GetCameraTransform().CameraFromRender(
|
||||
proj->SampleTime(0.5f));
|
||||
const Transform &worldFromShape = use.worldFromShape;
|
||||
Bounds2i pb = proj->GetFilm().PixelBounds();
|
||||
|
||||
constexpr Float kMinW = 1e-6f;
|
||||
|
||||
auto lodForTriangle = [&](const ImageTextureMeshTriangle &tri) -> Float {
|
||||
Point3f pc0 = cameraFromRender(worldFromShape(tri.p0));
|
||||
Point3f pc1 = cameraFromRender(worldFromShape(tri.p1));
|
||||
Point3f pc2 = cameraFromRender(worldFromShape(tri.p2));
|
||||
|
||||
Float qx0, qy0, qw0, qx1, qy1, qw1, qx2, qy2, qw2;
|
||||
MultiplyMatrixPointHomogeneous(M, pc0, &qx0, &qy0, &qw0);
|
||||
MultiplyMatrixPointHomogeneous(M, pc1, &qx1, &qy1, &qw1);
|
||||
MultiplyMatrixPointHomogeneous(M, pc2, &qx2, &qy2, &qw2);
|
||||
|
||||
if (qw0 <= kMinW || qw1 <= kMinW || qw2 <= kMinW)
|
||||
return Infinity;
|
||||
|
||||
Float xr0 = qx0 / qw0, yr0 = qy0 / qw0;
|
||||
Float xr1 = qx1 / qw1, yr1 = qy1 / qw1;
|
||||
Float xr2 = qx2 / qw2, yr2 = qy2 / qw2;
|
||||
|
||||
Float triMinX = std::min({xr0, xr1, xr2});
|
||||
Float triMaxX = std::max({xr0, xr1, xr2});
|
||||
Float triMinY = std::min({yr0, yr1, yr2});
|
||||
Float triMaxY = std::max({yr0, yr1, yr2});
|
||||
if (triMaxX < Float(pb.pMin.x) || triMinX >= Float(pb.pMax.x) ||
|
||||
triMaxY < Float(pb.pMin.y) || triMinY >= Float(pb.pMax.y))
|
||||
return Infinity;
|
||||
|
||||
Float u0 = tri.uv0.x, v0 = tri.uv0.y;
|
||||
Float u1 = tri.uv1.x, v1 = tri.uv1.y;
|
||||
Float u2 = tri.uv2.x, v2 = tri.uv2.y;
|
||||
|
||||
Float Iw0 = 1.f / qw0, Iw1 = 1.f / qw1, Iw2 = 1.f / qw2;
|
||||
Float Uow0 = u0 * Iw0, Uow1 = u1 * Iw1, Uow2 = u2 * Iw2;
|
||||
Float Vow0 = v0 * Iw0, Vow1 = v1 * Iw1, Vow2 = v2 * Iw2;
|
||||
|
||||
Float dUow_dx, dUow_dy, dVow_dx, dVow_dy, dIw_dx, dIw_dy;
|
||||
if (!AffineTripleScalarGradients2D(xr0, yr0, xr1, yr1, xr2, yr2, Uow0, Uow1, Uow2,
|
||||
&dUow_dx, &dUow_dy, Vow0, Vow1, Vow2, &dVow_dx,
|
||||
&dVow_dy, Iw0, Iw1, Iw2, &dIw_dx, &dIw_dy))
|
||||
return Infinity;
|
||||
|
||||
Float inv_w = (Iw0 + Iw1 + Iw2) / 3.f;
|
||||
if (!(inv_w >= kMinW) || !IsFinite(inv_w))
|
||||
return Infinity;
|
||||
|
||||
Float sumUow = Uow0 + Uow1 + Uow2;
|
||||
Float sumVow = Vow0 + Vow1 + Vow2;
|
||||
Float sumIw = Iw0 + Iw1 + Iw2;
|
||||
if (sumIw <= kMinW)
|
||||
return Infinity;
|
||||
Float u = sumUow / sumIw;
|
||||
Float v = sumVow / sumIw;
|
||||
|
||||
Float dudx = (dUow_dx - u * dIw_dx) / inv_w;
|
||||
Float dvdx = (dVow_dx - v * dIw_dx) / inv_w;
|
||||
Float dudy = (dUow_dy - u * dIw_dy) / inv_w;
|
||||
Float dvdy = (dVow_dy - v * dIw_dy) / inv_w;
|
||||
|
||||
if (!IsFinite(dudx) || !IsFinite(dvdx) || !IsFinite(dudy) || !IsFinite(dvdy))
|
||||
return Infinity;
|
||||
|
||||
Float dsdx = use.su * dudx, dsdy = use.su * dudy;
|
||||
Float dtdx = use.sv * dvdx, dtdy = use.sv * dvdy;
|
||||
|
||||
return ImageTextureContinuousLOD(use.filter, Vector2f(dsdx, dtdx),
|
||||
Vector2f(dsdy, dtdy), use.maxAnisotropy, pyramidLevels);
|
||||
};
|
||||
|
||||
const std::vector<ImageTextureMeshTriangle> &tris = *use.localTriangles;
|
||||
const int64_t nTris = (int64_t)tris.size();
|
||||
static constexpr int64_t kParallelMinTrianglesForLod = 4096;
|
||||
Float minLod = Infinity;
|
||||
|
||||
if (nTris >= kParallelMinTrianglesForLod && RunningThreads() > 1) {
|
||||
std::atomic<Float> minLodAtomic{Infinity};
|
||||
ParallelFor(0, nTris, [&](int64_t ti) {
|
||||
Float lod = lodForTriangle(tris[(size_t)ti]);
|
||||
if (!IsFinite(lod))
|
||||
return;
|
||||
Float cur = minLodAtomic.load(std::memory_order_relaxed);
|
||||
while (lod < cur) {
|
||||
if (minLodAtomic.compare_exchange_weak(cur, lod, std::memory_order_relaxed,
|
||||
std::memory_order_relaxed))
|
||||
break;
|
||||
}
|
||||
});
|
||||
minLod = minLodAtomic.load(std::memory_order_relaxed);
|
||||
} else {
|
||||
for (const ImageTextureMeshTriangle &tri : tris) {
|
||||
Float lod = lodForTriangle(tri);
|
||||
if (IsFinite(lod))
|
||||
minLod = std::min(minLod, lod);
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsFinite(minLod))
|
||||
return 0;
|
||||
return minLod;
|
||||
}
|
||||
|
||||
int ComputeImageTextureSafeDownsizesFromPreprocess(
|
||||
const Camera &camera, const std::vector<ImageTextureGeometryUse> &usesForTexture,
|
||||
int mipmapPyramidLevels, Allocator alloc) {
|
||||
if (usesForTexture.empty())
|
||||
return 0;
|
||||
|
||||
const std::string texLog =
|
||||
ShortScenePathForMipLog(usesForTexture[0].resolvedImageFilename);
|
||||
if (MipPreprocessLogDetail())
|
||||
Printf("[mip preprocess] texture \"%s\"\n", texLog.c_str());
|
||||
|
||||
const size_t n = usesForTexture.size();
|
||||
// ParallelFor + early bail rarely cut wall time here: most geometries yield pairSafe > 0 so
|
||||
// all LOD paths still run; when verbose logging is on, console I/O often dominates anyway.
|
||||
auto pairSafeFromMinLod = [&](Float minLod) {
|
||||
Float lodClamped = std::max<Float>(0, minLod);
|
||||
int ps = (int)std::floor(lodClamped + 1e-5f);
|
||||
if (mipmapPyramidLevels > 0)
|
||||
ps = std::min(ps, mipmapPyramidLevels - 1);
|
||||
return std::max(0, ps);
|
||||
};
|
||||
|
||||
std::vector<Float> minLods(n);
|
||||
std::vector<unsigned char> lodComputed(n, 0);
|
||||
std::atomic<bool> bailForZeroPairSafe{false};
|
||||
|
||||
ParallelFor(0, (int64_t)n, [&](int64_t ui) {
|
||||
size_t i = (size_t)ui;
|
||||
if (bailForZeroPairSafe.load(std::memory_order_relaxed))
|
||||
return;
|
||||
Float minLod =
|
||||
MinPrimaryContinuousLodForUse(camera, usesForTexture[i], mipmapPyramidLevels, alloc);
|
||||
minLods[i] = minLod;
|
||||
lodComputed[i] = 1;
|
||||
if (pairSafeFromMinLod(minLod) == 0)
|
||||
bailForZeroPairSafe.store(true, std::memory_order_relaxed);
|
||||
});
|
||||
|
||||
int textureMinSafeDownsizes = std::numeric_limits<int>::max();
|
||||
if (bailForZeroPairSafe.load(std::memory_order_acquire))
|
||||
textureMinSafeDownsizes = 0;
|
||||
else {
|
||||
for (size_t ui = 0; ui < n; ++ui) {
|
||||
CHECK(lodComputed[ui]);
|
||||
textureMinSafeDownsizes =
|
||||
std::min(textureMinSafeDownsizes, pairSafeFromMinLod(minLods[ui]));
|
||||
}
|
||||
}
|
||||
|
||||
if (MipPreprocessLogDetail()) {
|
||||
for (size_t ui = 0; ui < n; ++ui) {
|
||||
if (!lodComputed[ui])
|
||||
continue;
|
||||
const ImageTextureGeometryUse &use = usesForTexture[ui];
|
||||
Float minLod = minLods[ui];
|
||||
int pairSafe = pairSafeFromMinLod(minLod);
|
||||
|
||||
const std::string &geom =
|
||||
use.geometryDebugLabel.empty() ? std::string("(no label)") : use.geometryDebugLabel;
|
||||
Printf(" geometry \"%s\" -> safe downsizes %d (min primary LOD %.4f)\n", geom, pairSafe,
|
||||
minLod);
|
||||
|
||||
if (pairSafe == 0) {
|
||||
size_t remaining = n - ui - 1;
|
||||
if (remaining > 0)
|
||||
Printf(
|
||||
" (... skipping %zu more geometries; cannot increase safe downsizes above 0)\n",
|
||||
remaining);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (textureMinSafeDownsizes == std::numeric_limits<int>::max())
|
||||
textureMinSafeDownsizes = 0;
|
||||
return std::max(0, textureMinSafeDownsizes);
|
||||
}
|
||||
|
||||
void RunImageTextureMipPreprocess(BasicScene &scene, const Camera &camera) {
|
||||
ClearImageTextureMipDownsizeOverrides();
|
||||
if (!Options || !Options->skipMipImageTextures)
|
||||
return;
|
||||
|
||||
Timer preprocessTimer;
|
||||
|
||||
std::map<std::string, SpectrumImagemapDeclarationInfo> decls = scene.SpectrumImagemapDeclarations();
|
||||
std::vector<std::string> files = scene.CollectResolvedImageTextureFilenames();
|
||||
std::unordered_map<std::string, std::string> pathKeyToCanonical = BuildPathKeyToCanonicalMap(files);
|
||||
Allocator alloc;
|
||||
|
||||
std::unordered_map<std::string, TriQuadMesh> plyCache;
|
||||
std::unordered_map<std::string, std::vector<ImageTextureGeometryUse>> usesByFile =
|
||||
GatherImageTextureUsesByFile(scene, decls, pathKeyToCanonical, &plyCache);
|
||||
|
||||
// One LOD computation per path-equivalence class (Windows may list the same path twice with
|
||||
// different spellings); apply the result to every matching entry in files.
|
||||
std::vector<char> fileDone(files.size(), 0);
|
||||
for (size_t fi = 0; fi < files.size(); ++fi) {
|
||||
if (fileDone[fi])
|
||||
continue;
|
||||
const std::string &repFn = files[fi];
|
||||
|
||||
std::string canonicalFn = CanonicalTexturePathForMip(repFn, pathKeyToCanonical);
|
||||
std::vector<ImageTextureGeometryUse> uses;
|
||||
auto useIt = usesByFile.find(canonicalFn);
|
||||
if (useIt != usesByFile.end()) {
|
||||
uses = std::move(useIt->second);
|
||||
usesByFile.erase(useIt);
|
||||
}
|
||||
|
||||
Point2i res = Image::ReadResolution(repFn);
|
||||
int pyramidLevels = MipmapPyramidLevelsForImageResolution(res);
|
||||
|
||||
int safeDownsizes = 0;
|
||||
if (uses.empty()) {
|
||||
if (MipPreprocessLogDetail()) {
|
||||
Printf("[mip preprocess] texture \"%s\"\n", ShortScenePathForMipLog(repFn));
|
||||
Printf(
|
||||
" (no trianglemesh/plymesh reflectance imagemap uses found; safe downsizes 0)\n");
|
||||
}
|
||||
} else {
|
||||
safeDownsizes =
|
||||
ComputeImageTextureSafeDownsizesFromPreprocess(camera, uses, pyramidLevels, alloc);
|
||||
}
|
||||
|
||||
if (MipPreprocessLogDetail())
|
||||
Printf(" final safe downsizes %d\n", safeDownsizes);
|
||||
|
||||
for (size_t fj = fi; fj < files.size(); ++fj) {
|
||||
if (fileDone[fj])
|
||||
continue;
|
||||
if (!ResolvedImageTexturePathsEqual(repFn, files[fj]))
|
||||
continue;
|
||||
fileDone[fj] = 1;
|
||||
SetImageTextureMipDownsizeOverrideForFile(files[fj], safeDownsizes);
|
||||
}
|
||||
}
|
||||
|
||||
if (MipPreprocessLogDetail())
|
||||
Printf("[mip preprocess] wall time %.3f s\n", preprocessTimer.ElapsedSeconds());
|
||||
else
|
||||
// Keep "wall time" in the line so compare-skipmip.ps1 can parse preprocess duration.
|
||||
Printf("[mip preprocess] wall time %.3f s (%zu image textures)\n",
|
||||
preprocessTimer.ElapsedSeconds(), files.size());
|
||||
}
|
||||
|
||||
} // namespace pbrt
|
||||
59
src/pbrt/texture_mip_preprocess.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// pbrt is Copyright(c) 1998-2020 Matt Pharr, Wenzel Jakob, and Greg Humphreys.
|
||||
// The pbrt source code is licensed under the Apache License, Version 2.0.
|
||||
// SPDX: Apache-2.0
|
||||
|
||||
#ifndef PBRT_TEXTURE_MIP_PREPROCESS_H
|
||||
#define PBRT_TEXTURE_MIP_PREPROCESS_H
|
||||
|
||||
#include <pbrt/pbrt.h>
|
||||
|
||||
#include <pbrt/util/mipmap.h>
|
||||
#include <pbrt/util/transform.h>
|
||||
#include <pbrt/util/vecmath.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace pbrt {
|
||||
|
||||
class BasicScene;
|
||||
class Camera;
|
||||
|
||||
// One shaded triangle in render space with parametric UVs (matches TriangleMesh usage).
|
||||
struct ImageTextureMeshTriangle {
|
||||
Point3f p0, p1, p2;
|
||||
Point2f uv0, uv1, uv2;
|
||||
};
|
||||
|
||||
// Geometry using one named imagemap on diffuse materials (UV mapping only for now).
|
||||
// Mesh vertices are in shape/object space; worldFromShape maps to render space (same convention
|
||||
// as before when triangles were stored world-transformed). Multiple uses may share localTriangles
|
||||
// (several textures on one mesh, or many instances of one definition shape).
|
||||
struct ImageTextureGeometryUse {
|
||||
std::string resolvedImageFilename;
|
||||
std::string geometryDebugLabel;
|
||||
std::shared_ptr<const std::vector<ImageTextureMeshTriangle>> localTriangles;
|
||||
Transform worldFromShape;
|
||||
Float su = 1, sv = 1, du = 0, dv = 0;
|
||||
Float maxAnisotropy = 8.f;
|
||||
FilterFunction filter = FilterFunction::Bilinear;
|
||||
};
|
||||
|
||||
// Per-geometry safe downsizes = floor(min primary continuous LOD); texture override =
|
||||
// min over geometries. Primary visibility only (analytic screen-space UV derivatives for
|
||||
// perspective/orthographic; spherical/realistic cameras yield 0 safe downsizes). UV imagemap
|
||||
// on reflectance for diffuse / coateddiffuse / diffusetransmission and mix thereof;
|
||||
// trianglemesh + plymesh; includes ObjectInstance placements (transformed into render space).
|
||||
// Independent of integrator samples per pixel (screen-to-texture footprint uses the pixel grid).
|
||||
int ComputeImageTextureSafeDownsizesFromPreprocess(
|
||||
const Camera &camera, const std::vector<ImageTextureGeometryUse> &usesForTexture,
|
||||
int mipmapPyramidLevels, Allocator alloc);
|
||||
|
||||
// Clears prior overrides, then assigns per-file safe downsizes before image loads.
|
||||
// No-op when --skipmip is off (aside from clearing stale overrides).
|
||||
void RunImageTextureMipPreprocess(BasicScene &scene, const Camera &camera);
|
||||
|
||||
} // namespace pbrt
|
||||
|
||||
#endif // PBRT_TEXTURE_MIP_PREPROCESS_H
|
||||
|
|
@ -400,8 +400,9 @@ std::string FloatImageTexture::ToString() const {
|
|||
|
||||
std::string TexInfo::ToString() const {
|
||||
return StringPrintf(
|
||||
"[ TexInfo filename: %s filterOptions: %s wrapMode: %s encoding: %s ]", filename,
|
||||
filterOptions, wrapMode, encoding);
|
||||
"[ TexInfo filename: %s filterOptions: %s wrapMode: %s encoding: %s "
|
||||
"baseMipDownsizeSteps: %d ]",
|
||||
filename, filterOptions, wrapMode, encoding, baseMipDownsizeSteps);
|
||||
}
|
||||
|
||||
std::mutex ImageTextureBase::textureCacheMutex;
|
||||
|
|
@ -1010,7 +1011,8 @@ static std::map<std::string, RGBTextureCacheItem> rgbTextureCache;
|
|||
STAT_MEMORY_COUNTER("Memory/ImageTextures", gpuImageTextureBytes);
|
||||
|
||||
static cudaMipmappedArray_t createSingleChannelTextureArray(
|
||||
const Image &image, const RGBColorSpace *colorSpace, int *nMIPMapLevels) {
|
||||
const Image &image, const RGBColorSpace *colorSpace, int *nMIPMapLevels,
|
||||
const std::string &reportGPUPathForStats) {
|
||||
CHECK_EQ(1, image.NChannels());
|
||||
cudaMipmappedArray_t mipArray;
|
||||
|
||||
|
|
@ -1030,7 +1032,8 @@ static cudaMipmappedArray_t createSingleChannelTextureArray(
|
|||
}
|
||||
|
||||
MIPMap mipmap(image, colorSpace, WrapMode::Clamp /* TODO */, Allocator(),
|
||||
MIPMapFilterOptions());
|
||||
MIPMapFilterOptions(),
|
||||
ImageTextureMipDownsizeStepsForFile(reportGPUPathForStats));
|
||||
*nMIPMapLevels = mipmap.Levels();
|
||||
|
||||
const Image &baseImage = mipmap.GetLevel(0);
|
||||
|
|
@ -1039,6 +1042,7 @@ static cudaMipmappedArray_t createSingleChannelTextureArray(
|
|||
CUDA_CHECK(cudaMallocMipmappedArray(&mipArray, &channelDesc, extent, mipmap.Levels(),
|
||||
0 /* flags */));
|
||||
|
||||
int64_t deviceBytesThisTexture = 0;
|
||||
for (int level = 0; level < mipmap.Levels(); ++level) {
|
||||
const Image &levelImage = mipmap.GetLevel(level);
|
||||
cudaArray_t levelArray;
|
||||
|
|
@ -1059,13 +1063,18 @@ static cudaMipmappedArray_t createSingleChannelTextureArray(
|
|||
LOG_FATAL("Unhandled PixelFormat");
|
||||
}
|
||||
|
||||
gpuImageTextureBytes += pitch * levelImage.Resolution().y;
|
||||
int64_t levelBytes = int64_t(pitch) * int64_t(levelImage.Resolution().y);
|
||||
deviceBytesThisTexture += levelBytes;
|
||||
gpuImageTextureBytes += levelBytes;
|
||||
|
||||
CUDA_CHECK(cudaMemcpy2DToArray(
|
||||
levelArray, /* offset */ 0, 0, levelImage.RawPointer({0, 0}), pitch, pitch,
|
||||
levelImage.Resolution().y, cudaMemcpyHostToDevice));
|
||||
}
|
||||
|
||||
if (!reportGPUPathForStats.empty())
|
||||
ReportImageTextureMemory(reportGPUPathForStats, deviceBytesThisTexture, "GPU");
|
||||
|
||||
return mipArray;
|
||||
}
|
||||
|
||||
|
|
@ -1131,20 +1140,21 @@ GPUSpectrumImageTexture *GPUSpectrumImageTexture::Create(
|
|||
textureCacheMutex.unlock();
|
||||
|
||||
{
|
||||
ImageAndMetadata immeta = Image::Read(filename);
|
||||
ImageAndMetadata immeta = Image::Read(filename, alloc, encoding);
|
||||
Image &image = immeta.image;
|
||||
|
||||
readMode = image.Format() == PixelFormat::U256
|
||||
? cudaReadModeNormalizedFloat
|
||||
: cudaReadModeElementType;
|
||||
colorSpace = immeta.metadata.GetColorSpace();
|
||||
|
||||
ImageChannelDesc rgbDesc = image.GetChannelDesc({"R", "G", "B"});
|
||||
if (rgbDesc) {
|
||||
image = image.SelectChannels(rgbDesc);
|
||||
readMode = image.Format() == PixelFormat::U256
|
||||
? cudaReadModeNormalizedFloat
|
||||
: cudaReadModeElementType;
|
||||
|
||||
MIPMap mipmap(image, colorSpace, WrapMode::Clamp /* TODO */,
|
||||
Allocator(), MIPMapFilterOptions());
|
||||
Allocator(), MIPMapFilterOptions(),
|
||||
ImageTextureMipDownsizeStepsForFile(filename));
|
||||
nMIPMapLevels = mipmap.Levels();
|
||||
const Image &baseImage = mipmap.GetLevel(0);
|
||||
|
||||
|
|
@ -1158,6 +1168,7 @@ GPUSpectrumImageTexture *GPUSpectrumImageTexture::Create(
|
|||
CUDA_CHECK(cudaMallocMipmappedArray(&mipArray, &channelDesc,
|
||||
extent, mipmap.Levels(),
|
||||
0 /* flags */));
|
||||
int64_t gpuRgbDeviceBytes = 0;
|
||||
for (int level = 0; level < mipmap.Levels(); ++level) {
|
||||
const Image &levelImage = mipmap.GetLevel(level);
|
||||
cudaArray_t levelArray;
|
||||
|
|
@ -1176,13 +1187,16 @@ GPUSpectrumImageTexture *GPUSpectrumImageTexture::Create(
|
|||
}
|
||||
|
||||
int pitch = levelImage.Resolution().x * 4 * sizeof(uint8_t);
|
||||
gpuImageTextureBytes += pitch * levelImage.Resolution().y;
|
||||
int64_t lb = int64_t(pitch) * int64_t(levelImage.Resolution().y);
|
||||
gpuRgbDeviceBytes += lb;
|
||||
gpuImageTextureBytes += lb;
|
||||
|
||||
CUDA_CHECK(cudaMemcpy2DToArray(
|
||||
levelArray,
|
||||
/* offset */ 0, 0, rgba.data(), pitch, pitch,
|
||||
levelImage.Resolution().y, cudaMemcpyHostToDevice));
|
||||
}
|
||||
ReportImageTextureMemory(filename, gpuRgbDeviceBytes, "GPU");
|
||||
break;
|
||||
}
|
||||
case PixelFormat::Half: {
|
||||
|
|
@ -1195,6 +1209,7 @@ GPUSpectrumImageTexture *GPUSpectrumImageTexture::Create(
|
|||
extent, mipmap.Levels(),
|
||||
0 /* flags */));
|
||||
|
||||
int64_t gpuRgbDeviceBytes = 0;
|
||||
for (int level = 0; level < mipmap.Levels(); ++level) {
|
||||
const Image &levelImage = mipmap.GetLevel(level);
|
||||
cudaArray_t levelArray;
|
||||
|
|
@ -1214,13 +1229,16 @@ GPUSpectrumImageTexture *GPUSpectrumImageTexture::Create(
|
|||
}
|
||||
|
||||
int pitch = levelImage.Resolution().x * 4 * sizeof(Half);
|
||||
gpuImageTextureBytes += pitch * levelImage.Resolution().y;
|
||||
int64_t lb = int64_t(pitch) * int64_t(levelImage.Resolution().y);
|
||||
gpuRgbDeviceBytes += lb;
|
||||
gpuImageTextureBytes += lb;
|
||||
|
||||
CUDA_CHECK(cudaMemcpy2DToArray(
|
||||
levelArray,
|
||||
/* offset */ 0, 0, rgba.data(), pitch, pitch,
|
||||
levelImage.Resolution().y, cudaMemcpyHostToDevice));
|
||||
}
|
||||
ReportImageTextureMemory(filename, gpuRgbDeviceBytes, "GPU");
|
||||
break;
|
||||
}
|
||||
case PixelFormat::Float: {
|
||||
|
|
@ -1233,6 +1251,7 @@ GPUSpectrumImageTexture *GPUSpectrumImageTexture::Create(
|
|||
extent, mipmap.Levels(),
|
||||
0 /* flags */));
|
||||
|
||||
int64_t gpuRgbDeviceBytes = 0;
|
||||
for (int level = 0; level < mipmap.Levels(); ++level) {
|
||||
const Image &levelImage = mipmap.GetLevel(level);
|
||||
cudaArray_t levelArray;
|
||||
|
|
@ -1251,13 +1270,16 @@ GPUSpectrumImageTexture *GPUSpectrumImageTexture::Create(
|
|||
}
|
||||
|
||||
int pitch = levelImage.Resolution().x * 4 * sizeof(float);
|
||||
gpuImageTextureBytes += pitch * levelImage.Resolution().y;
|
||||
int64_t lb = int64_t(pitch) * int64_t(levelImage.Resolution().y);
|
||||
gpuRgbDeviceBytes += lb;
|
||||
gpuImageTextureBytes += lb;
|
||||
|
||||
CUDA_CHECK(cudaMemcpy2DToArray(
|
||||
levelArray,
|
||||
/* offset */ 0, 0, rgba.data(), pitch, pitch,
|
||||
levelImage.Resolution().y, cudaMemcpyHostToDevice));
|
||||
}
|
||||
ReportImageTextureMemory(filename, gpuRgbDeviceBytes, "GPU");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
@ -1269,8 +1291,11 @@ GPUSpectrumImageTexture *GPUSpectrumImageTexture::Create(
|
|||
mipArray, readMode, nMIPMapLevels, colorSpace};
|
||||
textureCacheMutex.unlock();
|
||||
} else if (image.NChannels() == 1) {
|
||||
readMode = image.Format() == PixelFormat::U256
|
||||
? cudaReadModeNormalizedFloat
|
||||
: cudaReadModeElementType;
|
||||
mipArray = createSingleChannelTextureArray(image, colorSpace,
|
||||
&nMIPMapLevels);
|
||||
&nMIPMapLevels, filename);
|
||||
|
||||
textureCacheMutex.lock();
|
||||
lumTextureCache[filename] = LuminanceTextureCacheItem{
|
||||
|
|
@ -1357,7 +1382,7 @@ GPUFloatImageTexture *GPUFloatImageTexture::Create(
|
|||
} else {
|
||||
textureCacheMutex.unlock();
|
||||
|
||||
ImageAndMetadata immeta = Image::Read(filename);
|
||||
ImageAndMetadata immeta = Image::Read(filename, alloc, encoding);
|
||||
Image &image = immeta.image;
|
||||
const RGBColorSpace *colorSpace = immeta.metadata.GetColorSpace();
|
||||
|
||||
|
|
@ -1393,7 +1418,8 @@ GPUFloatImageTexture *GPUFloatImageTexture::Create(
|
|||
image.NChannels());
|
||||
}
|
||||
|
||||
mipArray = createSingleChannelTextureArray(image, colorSpace, &nMIPMapLevels);
|
||||
mipArray =
|
||||
createSingleChannelTextureArray(image, colorSpace, &nMIPMapLevels, filename);
|
||||
readMode = (image.Format() == PixelFormat::U256) ? cudaReadModeNormalizedFloat
|
||||
: cudaReadModeElementType;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
#include <pbrt/pbrt.h>
|
||||
|
||||
#include <pbrt/options.h>
|
||||
#include <pbrt/base/texture.h>
|
||||
#include <pbrt/interaction.h>
|
||||
#include <pbrt/paramdict.h>
|
||||
|
|
@ -505,12 +506,17 @@ class FBmTexture {
|
|||
struct TexInfo {
|
||||
// TexInfo Public Methods
|
||||
TexInfo(const std::string &f, MIPMapFilterOptions filterOptions, WrapMode wm,
|
||||
ColorEncoding encoding)
|
||||
: filename(f), filterOptions(filterOptions), wrapMode(wm), encoding(encoding) {}
|
||||
ColorEncoding encoding, int baseMipDownsizeSteps)
|
||||
: filename(f),
|
||||
filterOptions(filterOptions),
|
||||
wrapMode(wm),
|
||||
encoding(encoding),
|
||||
baseMipDownsizeSteps(baseMipDownsizeSteps) {}
|
||||
|
||||
bool operator<(const TexInfo &t) const {
|
||||
return std::tie(filename, filterOptions, encoding, wrapMode) <
|
||||
std::tie(t.filename, t.filterOptions, t.encoding, t.wrapMode);
|
||||
return std::tie(filename, filterOptions, encoding, wrapMode, baseMipDownsizeSteps) <
|
||||
std::tie(t.filename, t.filterOptions, t.encoding, t.wrapMode,
|
||||
t.baseMipDownsizeSteps);
|
||||
}
|
||||
|
||||
std::string ToString() const;
|
||||
|
|
@ -519,6 +525,7 @@ struct TexInfo {
|
|||
MIPMapFilterOptions filterOptions;
|
||||
WrapMode wrapMode;
|
||||
ColorEncoding encoding;
|
||||
int baseMipDownsizeSteps;
|
||||
};
|
||||
|
||||
// ImageTextureBase Definition
|
||||
|
|
@ -527,10 +534,11 @@ class ImageTextureBase {
|
|||
// ImageTextureBase Public Methods
|
||||
ImageTextureBase(TextureMapping2D mapping, std::string filename,
|
||||
MIPMapFilterOptions filterOptions, WrapMode wrapMode, Float scale,
|
||||
bool invert, ColorEncoding encoding, Allocator alloc)
|
||||
bool invert, ColorEncoding encoding, Allocator alloc,
|
||||
int baseMipDownsizeSteps)
|
||||
: mapping(mapping), filename(filename), scale(scale), invert(invert) {
|
||||
// Get _MIPMap_ from texture cache if present
|
||||
TexInfo texInfo(filename, filterOptions, wrapMode, encoding);
|
||||
TexInfo texInfo(filename, filterOptions, wrapMode, encoding, baseMipDownsizeSteps);
|
||||
std::unique_lock<std::mutex> lock(textureCacheMutex);
|
||||
if (auto iter = textureCache.find(texInfo); iter != textureCache.end()) {
|
||||
mipmap = iter->second;
|
||||
|
|
@ -539,8 +547,8 @@ class ImageTextureBase {
|
|||
lock.unlock();
|
||||
|
||||
// Create _MIPMap_ for _filename_ and add to texture cache
|
||||
mipmap =
|
||||
MIPMap::CreateFromFile(filename, filterOptions, wrapMode, encoding, alloc);
|
||||
mipmap = MIPMap::CreateFromFile(filename, filterOptions, wrapMode, encoding, alloc,
|
||||
baseMipDownsizeSteps);
|
||||
lock.lock();
|
||||
// This is actually ok, but if it hits, it means we've wastefully
|
||||
// loaded this texture. (Note that in that case, should just return
|
||||
|
|
@ -573,8 +581,8 @@ class FloatImageTexture : public ImageTextureBase {
|
|||
FloatImageTexture(TextureMapping2D m, const std::string &filename,
|
||||
MIPMapFilterOptions filterOptions, WrapMode wm, Float scale,
|
||||
bool invert, ColorEncoding encoding, Allocator alloc)
|
||||
: ImageTextureBase(m, filename, filterOptions, wm, scale, invert, encoding,
|
||||
alloc) {}
|
||||
: ImageTextureBase(m, filename, filterOptions, wm, scale, invert, encoding, alloc,
|
||||
ImageTextureMipDownsizeStepsForFile(filename)) {}
|
||||
PBRT_CPU_GPU
|
||||
Float Evaluate(TextureEvalContext ctx) const {
|
||||
#ifdef PBRT_IS_GPU_CODE
|
||||
|
|
@ -606,7 +614,7 @@ class SpectrumImageTexture : public ImageTextureBase {
|
|||
Float scale, bool invert, ColorEncoding encoding,
|
||||
SpectrumType spectrumType, Allocator alloc)
|
||||
: ImageTextureBase(mapping, filename, filterOptions, wrapMode, scale, invert,
|
||||
encoding, alloc),
|
||||
encoding, alloc, ImageTextureMipDownsizeStepsForFile(filename)),
|
||||
spectrumType(spectrumType) {}
|
||||
|
||||
PBRT_CPU_GPU
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#ifndef PBRT_IS_WINDOWS
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
|
|
@ -292,4 +293,51 @@ bool WriteFileContents(std::string filename, const std::string &contents) {
|
|||
return true;
|
||||
}
|
||||
|
||||
static bool FilenameEqAsciiInsensitive(const std::string &a, const std::string &b) {
|
||||
if (a.size() != b.size())
|
||||
return false;
|
||||
for (size_t i = 0; i < a.size(); ++i)
|
||||
if (std::tolower(static_cast<unsigned char>(a[i])) !=
|
||||
std::tolower(static_cast<unsigned char>(b[i])))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void SplitPathComponents(const std::string &path,
|
||||
std::vector<std::string> *components) {
|
||||
components->clear();
|
||||
std::string cur;
|
||||
for (unsigned char uc : path) {
|
||||
char c = static_cast<char>(uc);
|
||||
if (c == '/' || c == '\\') {
|
||||
if (!cur.empty()) {
|
||||
components->push_back(cur);
|
||||
cur.clear();
|
||||
}
|
||||
} else
|
||||
cur += c;
|
||||
}
|
||||
if (!cur.empty())
|
||||
components->push_back(cur);
|
||||
}
|
||||
|
||||
std::string PathForImageTextureStats(std::string path) {
|
||||
std::vector<std::string> comps;
|
||||
SplitPathComponents(path, &comps);
|
||||
for (size_t i = 0; i < comps.size(); ++i) {
|
||||
if (!FilenameEqAsciiInsensitive(comps[i], "pbrt-v4-scenes"))
|
||||
continue;
|
||||
std::string rel;
|
||||
for (size_t j = i + 1; j < comps.size(); ++j) {
|
||||
if (!rel.empty())
|
||||
rel += '/';
|
||||
rel += comps[j];
|
||||
}
|
||||
if (!rel.empty())
|
||||
return rel;
|
||||
break;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
} // namespace pbrt
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ bool RemoveFile(std::string filename);
|
|||
std::string ResolveFilename(std::string filename);
|
||||
void SetSearchDirectory(std::string filename);
|
||||
|
||||
// If _path_ contains a path component named "pbrt-v4-scenes" (ASCII case-insensitive on
|
||||
// Windows), return the remainder using generic '/' separators; otherwise return _path_.
|
||||
std::string PathForImageTextureStats(std::string path);
|
||||
|
||||
bool HasExtension(std::string filename, std::string ext);
|
||||
std::string RemoveExtension(std::string filename);
|
||||
|
||||
|
|
|
|||
|
|
@ -1611,6 +1611,112 @@ static int readWord(FILE *fp, char *buffer, int bufferLength) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
static Point2i ReadPFMResolution(const std::string &filename) {
|
||||
char buffer[BUFFER_SIZE];
|
||||
FILE *fp = FOpenRead(filename);
|
||||
if (!fp)
|
||||
ErrorExit("%s: unable to open PFM file", filename);
|
||||
|
||||
if (readWord(fp, buffer, BUFFER_SIZE) == -1)
|
||||
ErrorExit("%s: unable to read PFM file", filename);
|
||||
if (strcmp(buffer, "Pf") != 0 && strcmp(buffer, "PF") != 0)
|
||||
ErrorExit("%s: unable to decode PFM file type \"%c%c\"", filename, buffer[0],
|
||||
buffer[1]);
|
||||
|
||||
int width, height;
|
||||
if (readWord(fp, buffer, BUFFER_SIZE) == -1 || !Atoi(buffer, &width))
|
||||
ErrorExit("%s: unable to decode PFM width", filename);
|
||||
if (readWord(fp, buffer, BUFFER_SIZE) == -1 || !Atoi(buffer, &height))
|
||||
ErrorExit("%s: unable to decode PFM height", filename);
|
||||
|
||||
fclose(fp);
|
||||
return Point2i(width, height);
|
||||
}
|
||||
|
||||
#ifndef PBRT_IS_GPU_CODE
|
||||
static Point2i ReadEXRResolution(const std::string &name) {
|
||||
try {
|
||||
Imf::InputFile file(name.c_str());
|
||||
Imath::Box2i dw = file.header().dataWindow();
|
||||
int width = dw.max.x - dw.min.x + 1;
|
||||
int height = dw.max.y - dw.min.y + 1;
|
||||
return Point2i(width, height);
|
||||
} catch (const std::exception &e) {
|
||||
ErrorExit("Unable to read EXR image dimensions \"%s\": %s", name, e.what());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static Point2i ReadPNGResolution(const std::string &name) {
|
||||
constexpr size_t kInspectBytes = 262144;
|
||||
FILE *fp = FOpenRead(name);
|
||||
if (!fp)
|
||||
ErrorExit("%s: unable to open PNG file", name);
|
||||
pstd::vector<unsigned char> buf(kInspectBytes);
|
||||
size_t n = fread(buf.data(), 1, buf.size(), fp);
|
||||
fclose(fp);
|
||||
if (n < 29)
|
||||
ErrorExit("%s: truncated PNG file", name);
|
||||
|
||||
LodePNGState state;
|
||||
lodepng_state_init(&state);
|
||||
unsigned width = 0, height = 0;
|
||||
unsigned int error =
|
||||
lodepng_inspect(&width, &height, &state, buf.data(), n);
|
||||
lodepng_state_cleanup(&state);
|
||||
if (error != 0)
|
||||
ErrorExit("%s: %s", name, lodepng_error_text(error));
|
||||
return Point2i(int(width), int(height));
|
||||
}
|
||||
|
||||
static Point2i ReadQOIResolution(const std::string &filename) {
|
||||
FILE *fp = FOpenRead(filename);
|
||||
if (!fp)
|
||||
ErrorExit("%s: unable to open QOI file", filename);
|
||||
unsigned char b[12];
|
||||
if (fread(b, 1, 12, fp) != 12) {
|
||||
fclose(fp);
|
||||
ErrorExit("%s: truncated QOI file", filename);
|
||||
}
|
||||
fclose(fp);
|
||||
if (memcmp(b, "qoif", 4) != 0)
|
||||
ErrorExit("%s: invalid QOI magic", filename);
|
||||
unsigned w = (unsigned(b[4]) << 24) | (unsigned(b[5]) << 16) | (unsigned(b[6]) << 8) |
|
||||
unsigned(b[7]);
|
||||
unsigned h = (unsigned(b[8]) << 24) | (unsigned(b[9]) << 16) | (unsigned(b[10]) << 8) |
|
||||
unsigned(b[11]);
|
||||
if (w == 0 || h == 0)
|
||||
ErrorExit("%s: invalid QOI dimensions", filename);
|
||||
return Point2i(int(w), int(h));
|
||||
}
|
||||
|
||||
Point2i Image::ReadResolution(const std::string &name) {
|
||||
if (HasExtension(name, "exr")) {
|
||||
#ifndef PBRT_IS_GPU_CODE
|
||||
return ReadEXRResolution(name);
|
||||
#else
|
||||
ErrorExit("%s: ReadResolution(EXR) not supported in this build.", name);
|
||||
#endif
|
||||
}
|
||||
if (HasExtension(name, "png"))
|
||||
return ReadPNGResolution(name);
|
||||
if (HasExtension(name, "pfm"))
|
||||
return ReadPFMResolution(name);
|
||||
if (HasExtension(name, "hdr")) {
|
||||
int x, y, comp;
|
||||
if (!stbi_info(name.c_str(), &x, &y, &comp))
|
||||
ErrorExit("%s: %s", name, stbi_failure_reason());
|
||||
return Point2i(x, y);
|
||||
}
|
||||
if (HasExtension(name, "qoi"))
|
||||
return ReadQOIResolution(name);
|
||||
|
||||
int x, y, n;
|
||||
if (!stbi_info(name.c_str(), &x, &y, &n))
|
||||
ErrorExit("%s: %s", name, stbi_failure_reason());
|
||||
return Point2i(x, y);
|
||||
}
|
||||
|
||||
static ImageAndMetadata ReadPFM(const std::string &filename, Allocator alloc) {
|
||||
pstd::vector<float> rgb32(alloc);
|
||||
char buffer[BUFFER_SIZE];
|
||||
|
|
|
|||
|
|
@ -342,6 +342,9 @@ class Image {
|
|||
static ImageAndMetadata Read(std::string filename, Allocator alloc = {},
|
||||
ColorEncoding encoding = nullptr);
|
||||
|
||||
// Image dimensions only (no pixel decode). Used by mip preprocess and similar.
|
||||
static Point2i ReadResolution(const std::string &filename);
|
||||
|
||||
bool Write(std::string name, const ImageMetadata &metadata = {}) const;
|
||||
|
||||
Image ConvertToFormat(PixelFormat format, ColorEncoding encoding = nullptr) const;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <pbrt/util/mipmap.h>
|
||||
|
||||
#include <pbrt/options.h>
|
||||
#include <pbrt/util/stats.h>
|
||||
#include <pbrt/util/check.h>
|
||||
#include <pbrt/util/color.h>
|
||||
#include <pbrt/util/colorspace.h>
|
||||
|
|
@ -13,15 +14,92 @@
|
|||
#include <pbrt/util/log.h>
|
||||
#include <pbrt/util/math.h>
|
||||
#include <pbrt/util/print.h>
|
||||
#include <pbrt/util/stats.h>
|
||||
#include <pbrt/util/parallel.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace pbrt {
|
||||
|
||||
STAT_MEMORY_COUNTER("Memory/Image maps", imageMapBytes);
|
||||
|
||||
// Used when Options->skipMipImageTextures (--skipmip) and no per-file override was installed
|
||||
// by RunImageTextureMipPreprocess (see texture_mip_preprocess.cpp).
|
||||
static constexpr int kDefaultImageTextureSkipMipLevelsWhenSkipMipEnabled = 3;
|
||||
|
||||
static std::mutex imageTextureMipOverrideMutex;
|
||||
static std::unordered_map<std::string, int> imageTextureMipOverrides;
|
||||
|
||||
void ClearImageTextureMipDownsizeOverrides() {
|
||||
std::lock_guard<std::mutex> lock(imageTextureMipOverrideMutex);
|
||||
imageTextureMipOverrides.clear();
|
||||
}
|
||||
|
||||
void SetImageTextureMipDownsizeOverrideForFile(const std::string &resolvedFilename, int steps) {
|
||||
std::lock_guard<std::mutex> lock(imageTextureMipOverrideMutex);
|
||||
imageTextureMipOverrides[resolvedFilename] = steps;
|
||||
}
|
||||
|
||||
int ImageTextureMipDownsizeStepsForFile(const std::string &resolvedFilename) {
|
||||
if (!Options || !Options->skipMipImageTextures)
|
||||
return 0;
|
||||
std::lock_guard<std::mutex> lock(imageTextureMipOverrideMutex);
|
||||
if (auto it = imageTextureMipOverrides.find(resolvedFilename);
|
||||
it != imageTextureMipOverrides.end())
|
||||
return std::max(0, it->second);
|
||||
return std::max(0, kDefaultImageTextureSkipMipLevelsWhenSkipMipEnabled);
|
||||
}
|
||||
|
||||
// Box-filter downsample by 2 in each dimension. Keeps the source pixel format (U256 / Half /
|
||||
// Float) so stored mip pyramids and Memory/Image maps reflect a real memory win; converting
|
||||
// everything to float here makes footprint ~unchanged (4× smaller area, ~4× larger texels).
|
||||
static Image HalveImageBoxFilter(Image image, Allocator alloc) {
|
||||
Point2i res = image.Resolution();
|
||||
Point2i half(std::max(1, res.x / 2), std::max(1, res.y / 2));
|
||||
if (half.x == res.x && half.y == res.y)
|
||||
return image;
|
||||
|
||||
int nc = image.NChannels();
|
||||
CHECK_GE(nc, 1);
|
||||
std::vector<std::string> chNames = image.ChannelNames();
|
||||
Image dst(image.Format(), half, pstd::MakeSpan(chNames), image.Encoding(), alloc);
|
||||
|
||||
ParallelFor2D(Bounds2i({0, 0}, half), [&](Bounds2i tile) {
|
||||
WrapMode2D clamp(WrapMode::Clamp);
|
||||
for (int y = tile.pMin.y; y < tile.pMax.y; ++y)
|
||||
for (int x = tile.pMin.x; x < tile.pMax.x; ++x) {
|
||||
ImageChannelValues sum(nc, Float(0));
|
||||
int count = 0;
|
||||
for (int dy = 0; dy < 2; ++dy)
|
||||
for (int dx = 0; dx < 2; ++dx) {
|
||||
int sx = 2 * x + dx;
|
||||
int sy = 2 * y + dy;
|
||||
if (sx < res.x && sy < res.y) {
|
||||
for (int c = 0; c < nc; ++c)
|
||||
sum[c] += image.GetChannel({sx, sy}, c, clamp);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
CHECK_GT(count, 0);
|
||||
Float inv = 1 / Float(count);
|
||||
for (int c = 0; c < nc; ++c)
|
||||
sum[c] *= inv;
|
||||
dst.SetChannels({x, y}, sum);
|
||||
}
|
||||
});
|
||||
return dst;
|
||||
}
|
||||
|
||||
// Apply _mipDownsizeSteps_ consecutive half-res box filters (each ~2× smaller per axis).
|
||||
static Image ApplyBaseMipDownsize(Image image, int mipDownsizeSteps, Allocator alloc) {
|
||||
for (int i = 0; i < mipDownsizeSteps; ++i)
|
||||
image = HalveImageBoxFilter(std::move(image), alloc);
|
||||
return image;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// MIPMap Helper Declarations
|
||||
|
||||
|
|
@ -46,6 +124,37 @@ std::string MIPMapFilterOptions::ToString() const {
|
|||
maxAnisotropy);
|
||||
}
|
||||
|
||||
int MipmapPyramidLevelsForImageResolution(Point2i resolution) {
|
||||
int w = RoundUpPow2(resolution.x);
|
||||
int h = RoundUpPow2(resolution.y);
|
||||
return 1 + Log2Int(std::max(w, h));
|
||||
}
|
||||
|
||||
Float EWAContinuousLOD(Vector2f dst0, Vector2f dst1, Float maxAnisotropy, int pyramidLevels) {
|
||||
if (pyramidLevels < 2)
|
||||
return 0;
|
||||
if (LengthSquared(dst0) < LengthSquared(dst1))
|
||||
std::swap(dst0, dst1);
|
||||
Float longerVecLength = Length(dst0), shorterVecLength = Length(dst1);
|
||||
if (shorterVecLength * maxAnisotropy < longerVecLength && shorterVecLength > 0) {
|
||||
Float scale = longerVecLength / (shorterVecLength * maxAnisotropy);
|
||||
dst1 *= scale;
|
||||
shorterVecLength *= scale;
|
||||
}
|
||||
if (shorterVecLength == 0)
|
||||
return 0;
|
||||
return std::max<Float>(0, pyramidLevels - 1 + Log2(shorterVecLength));
|
||||
}
|
||||
|
||||
Float ImageTextureContinuousLOD(FilterFunction filter, Vector2f dst0, Vector2f dst1,
|
||||
Float maxAnisotropy, int pyramidLevels) {
|
||||
if (filter == FilterFunction::EWA)
|
||||
return EWAContinuousLOD(dst0, dst1, maxAnisotropy, pyramidLevels);
|
||||
Float width =
|
||||
2 * std::max({std::abs(dst0[0]), std::abs(dst0[1]), std::abs(dst1[0]), std::abs(dst1[1])});
|
||||
return pyramidLevels - 1 + Log2(std::max<Float>(width, 1e-8f));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
|
|
@ -191,10 +300,15 @@ static PBRT_CONST Float MIPFilterLUT[MIPFilterLUTSize] = {
|
|||
};
|
||||
|
||||
// MIPMap Method Definitions
|
||||
MIPMap::MIPMap(Image image, const RGBColorSpace *colorSpace, WrapMode wrapMode,
|
||||
Allocator alloc, const MIPMapFilterOptions &options)
|
||||
MIPMap::MIPMap(Image image, const RGBColorSpace *colorSpace, WrapMode wrapMode, Allocator alloc,
|
||||
const MIPMapFilterOptions &options, int baseMipDownsizeSteps)
|
||||
: colorSpace(colorSpace), wrapMode(wrapMode), options(options) {
|
||||
CHECK(colorSpace);
|
||||
// Base mip downsize is applied here (not only in CreateFromFile) so all MIPMap
|
||||
// construction paths share the same behavior and memory accounting.
|
||||
int baseSteps = std::max(0, baseMipDownsizeSteps);
|
||||
if (baseSteps > 0)
|
||||
image = ApplyBaseMipDownsize(std::move(image), baseSteps, alloc);
|
||||
pyramid = Image::GeneratePyramid(std::move(image), wrapMode, alloc);
|
||||
if (Options->disableImageTextures) {
|
||||
Image top = pyramid.back();
|
||||
|
|
@ -205,6 +319,13 @@ MIPMap::MIPMap(Image image, const RGBColorSpace *colorSpace, WrapMode wrapMode,
|
|||
[](const Image &im) { imageMapBytes += im.BytesUsed(); });
|
||||
}
|
||||
|
||||
int64_t MIPMap::TotalBytesUsed() const {
|
||||
int64_t sum = 0;
|
||||
for (const Image &im : pyramid)
|
||||
sum += int64_t(im.BytesUsed());
|
||||
return sum;
|
||||
}
|
||||
|
||||
template <>
|
||||
Float MIPMap::Texel(int level, Point2i st) const {
|
||||
DCHECK(level >= 0 && level < pyramid.size());
|
||||
|
|
@ -350,7 +471,8 @@ T MIPMap::EWA(int level, Point2f st, Vector2f dst0, Vector2f dst1) const {
|
|||
|
||||
MIPMap *MIPMap::CreateFromFile(const std::string &filename,
|
||||
const MIPMapFilterOptions &options, WrapMode wrapMode,
|
||||
ColorEncoding encoding, Allocator alloc) {
|
||||
ColorEncoding encoding, Allocator alloc,
|
||||
int baseMipDownsizeSteps) {
|
||||
ImageAndMetadata imageAndMetadata = Image::Read(filename, alloc, encoding);
|
||||
|
||||
Image &image = imageAndMetadata.image;
|
||||
|
|
@ -378,8 +500,10 @@ MIPMap *MIPMap::CreateFromFile(const std::string &filename,
|
|||
}
|
||||
|
||||
const RGBColorSpace *colorSpace = imageAndMetadata.metadata.GetColorSpace();
|
||||
return alloc.new_object<MIPMap>(std::move(image), colorSpace, wrapMode, alloc,
|
||||
options);
|
||||
MIPMap *mipmap = alloc.new_object<MIPMap>(std::move(image), colorSpace, wrapMode, alloc,
|
||||
options, baseMipDownsizeSteps);
|
||||
ReportImageTextureMemory(filename, mipmap->TotalBytesUsed(), "CPU");
|
||||
return mipmap;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,24 @@ inline pstd::optional<FilterFunction> ParseFilter(const std::string &f) {
|
|||
|
||||
std::string ToString(FilterFunction f);
|
||||
|
||||
// Per-file mip downsize steps for --skipmip (each step is one 2× box-filter halving before the
|
||||
// mip pyramid). When --skipmip is off, always 0. When on, uses preprocess overrides if set,
|
||||
// otherwise kDefaultImageTextureSkipMipLevelsWhenSkipMipEnabled in mipmap.cpp.
|
||||
void ClearImageTextureMipDownsizeOverrides();
|
||||
void SetImageTextureMipDownsizeOverrideForFile(const std::string &resolvedFilename, int steps);
|
||||
int ImageTextureMipDownsizeStepsForFile(const std::string &resolvedFilename);
|
||||
|
||||
// Continuous EWA LOD matching MIPMap::Filter (EWA branch): level 0 is finest. Returns a value
|
||||
// >= 0; use with pyramidLevels from MipmapPyramidLevelsForImageResolution.
|
||||
Float EWAContinuousLOD(Vector2f dst0, Vector2f dst1, Float maxAnisotropy, int pyramidLevels);
|
||||
|
||||
// Continuous LOD for imagemap filtering (EWA or non-EWA mip selection in MIPMap::Filter).
|
||||
Float ImageTextureContinuousLOD(FilterFunction filter, Vector2f dst0, Vector2f dst1,
|
||||
Float maxAnisotropy, int pyramidLevels);
|
||||
|
||||
// Pyramid level count after Image::GeneratePyramid's power-of-two resize (matches runtime).
|
||||
int MipmapPyramidLevelsForImageResolution(Point2i resolution);
|
||||
|
||||
// MIPMapFilterOptions Definition
|
||||
struct MIPMapFilterOptions {
|
||||
FilterFunction filter = FilterFunction::EWA;
|
||||
|
|
@ -49,11 +67,12 @@ struct MIPMapFilterOptions {
|
|||
class MIPMap {
|
||||
public:
|
||||
// MIPMap Public Methods
|
||||
MIPMap(Image image, const RGBColorSpace *colorSpace, WrapMode wrapMode,
|
||||
Allocator alloc, const MIPMapFilterOptions &options);
|
||||
MIPMap(Image image, const RGBColorSpace *colorSpace, WrapMode wrapMode, Allocator alloc,
|
||||
const MIPMapFilterOptions &options, int baseMipDownsizeSteps);
|
||||
static MIPMap *CreateFromFile(const std::string &filename,
|
||||
const MIPMapFilterOptions &options, WrapMode wrapMode,
|
||||
ColorEncoding encoding, Allocator alloc);
|
||||
ColorEncoding encoding, Allocator alloc,
|
||||
int baseMipDownsizeSteps);
|
||||
|
||||
template <typename T>
|
||||
T Filter(Point2f st, Vector2f dstdx, Vector2f dstdy) const;
|
||||
|
|
@ -68,6 +87,8 @@ class MIPMap {
|
|||
const RGBColorSpace *GetRGBColorSpace() const { return colorSpace; }
|
||||
const Image &GetLevel(int level) const { return pyramid[level]; }
|
||||
|
||||
int64_t TotalBytesUsed() const;
|
||||
|
||||
private:
|
||||
// MIPMap Private Methods
|
||||
template <typename T>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include <pbrt/util/stats.h>
|
||||
|
||||
#include <pbrt/util/file.h>
|
||||
#include <pbrt/util/check.h>
|
||||
#include <pbrt/util/image.h>
|
||||
#include <pbrt/util/memory.h>
|
||||
|
|
@ -23,6 +24,31 @@
|
|||
|
||||
namespace pbrt {
|
||||
|
||||
static bool haveStatsRenderWallSeconds = false;
|
||||
static Float statsRenderWallSeconds = 0;
|
||||
|
||||
void SetStatsRenderWallSeconds(Float seconds) {
|
||||
haveStatsRenderWallSeconds = true;
|
||||
statsRenderWallSeconds = seconds;
|
||||
}
|
||||
|
||||
struct ImageTextureMemEntry {
|
||||
std::string path;
|
||||
int64_t bytes = 0;
|
||||
const char *where = "CPU";
|
||||
};
|
||||
|
||||
static std::mutex imageTextureMemMutex;
|
||||
static std::vector<ImageTextureMemEntry> imageTextureMemoryEntries;
|
||||
|
||||
void ReportImageTextureMemory(const std::string &path, int64_t bytes, const char *where) {
|
||||
if (bytes <= 0)
|
||||
return;
|
||||
std::lock_guard<std::mutex> lock(imageTextureMemMutex);
|
||||
imageTextureMemoryEntries.push_back(ImageTextureMemEntry{
|
||||
PathForImageTextureStats(path), bytes, where ? where : "CPU"});
|
||||
}
|
||||
|
||||
// ThreadStatsState Definition
|
||||
struct ThreadStatsState {
|
||||
Point2i p;
|
||||
|
|
@ -331,6 +357,8 @@ bool PrintCheckRare(FILE *dest) {
|
|||
|
||||
void ClearStats() {
|
||||
statsAccumulator.Clear();
|
||||
std::lock_guard<std::mutex> lock(imageTextureMemMutex);
|
||||
imageTextureMemoryEntries.clear();
|
||||
}
|
||||
|
||||
static void getCategoryAndTitle(const std::string &str, std::string *category,
|
||||
|
|
@ -349,16 +377,6 @@ void StatsAccumulator::Print(FILE *dest) {
|
|||
fprintf(dest, "Statistics:\n");
|
||||
std::map<std::string, std::vector<std::string>> toPrint;
|
||||
|
||||
for (auto &counter : stats->counters) {
|
||||
if (counter.second == 0)
|
||||
continue;
|
||||
std::string category, title;
|
||||
getCategoryAndTitle(counter.first, &category, &title);
|
||||
toPrint[category].push_back(
|
||||
StringPrintf("%-42s %12" PRIu64, title, counter.second));
|
||||
}
|
||||
|
||||
size_t totalMemoryReported = 0;
|
||||
auto printBytes = [](size_t bytes) -> std::string {
|
||||
float kb = (double)bytes / 1024.;
|
||||
if (std::abs(kb) < 1024.)
|
||||
|
|
@ -372,65 +390,56 @@ void StatsAccumulator::Print(FILE *dest) {
|
|||
return StringPrintf("%9.2f GiB", gib);
|
||||
};
|
||||
|
||||
// --stats: image texture memory (CPU host mipmaps + GPU device arrays) and per-file breakdown.
|
||||
int64_t cpuImageMapBytes = 0, gpuImageTexBytes = 0;
|
||||
for (auto &counter : stats->memoryCounters) {
|
||||
if (counter.second == 0)
|
||||
continue;
|
||||
totalMemoryReported += counter.second;
|
||||
if (counter.first == "Memory/Image maps")
|
||||
cpuImageMapBytes = counter.second;
|
||||
else if (counter.first == "Memory/ImageTextures")
|
||||
gpuImageTexBytes = counter.second;
|
||||
}
|
||||
|
||||
std::string category, title;
|
||||
getCategoryAndTitle(counter.first, &category, &title);
|
||||
toPrint[category].push_back(
|
||||
StringPrintf("%-42s %s", title, printBytes(counter.second)));
|
||||
}
|
||||
int64_t unreportedBytes = GetCurrentRSS() - totalMemoryReported;
|
||||
if (unreportedBytes > 0)
|
||||
toPrint["Memory"].push_back(StringPrintf("%-42s %s",
|
||||
"Unreported / unused",
|
||||
printBytes(unreportedBytes)));
|
||||
toPrint["Image textures"].push_back(StringPrintf(
|
||||
"%-6s %-58s %s", "", "CPU host (Image maps counter)", printBytes(cpuImageMapBytes)));
|
||||
if (gpuImageTexBytes > 0)
|
||||
toPrint["Image textures"].push_back(StringPrintf(
|
||||
"%-6s %-58s %s", "", "GPU device (ImageTextures counter)",
|
||||
printBytes(gpuImageTexBytes)));
|
||||
int64_t imageTexTotal = cpuImageMapBytes + gpuImageTexBytes;
|
||||
toPrint["Image textures"].push_back(StringPrintf(
|
||||
"%-6s %-58s %s", "", "Total (counters)", printBytes(imageTexTotal)));
|
||||
|
||||
for (auto &distrib : stats->intDistributions) {
|
||||
const std::string &name = distrib.first;
|
||||
if (distrib.second.count == 0)
|
||||
continue;
|
||||
std::string category, title;
|
||||
getCategoryAndTitle(name, &category, &title);
|
||||
double avg = (double)distrib.second.sum / (double)distrib.second.count;
|
||||
toPrint[category].push_back(StringPrintf(
|
||||
"%-42s %.3f avg [range %" PRIu64 " - %" PRIu64 "]",
|
||||
title, avg, distrib.second.min, distrib.second.max));
|
||||
std::vector<ImageTextureMemEntry> entriesCopy;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(imageTextureMemMutex);
|
||||
entriesCopy = imageTextureMemoryEntries;
|
||||
}
|
||||
for (auto &distrib : stats->floatDistributions) {
|
||||
const std::string &name = distrib.first;
|
||||
if (distrib.second.count == 0)
|
||||
continue;
|
||||
std::string category, title;
|
||||
getCategoryAndTitle(name, &category, &title);
|
||||
double avg = (double)distrib.second.sum / (double)distrib.second.count;
|
||||
toPrint[category].push_back(
|
||||
StringPrintf("%-42s %.3f avg [range %f - %f]", title,
|
||||
avg, distrib.second.min, distrib.second.max));
|
||||
std::sort(entriesCopy.begin(), entriesCopy.end(),
|
||||
[](const ImageTextureMemEntry &a, const ImageTextureMemEntry &b) {
|
||||
return a.bytes > b.bytes;
|
||||
});
|
||||
int64_t sumEntries = 0;
|
||||
for (const ImageTextureMemEntry &e : entriesCopy)
|
||||
sumEntries += e.bytes;
|
||||
if (!entriesCopy.empty()) {
|
||||
toPrint["Image textures"].push_back(StringPrintf(
|
||||
"%-6s %-58s %s", "", "Sum of per-path entries", printBytes(sumEntries)));
|
||||
toPrint["Image textures (by path)"].push_back(StringPrintf(
|
||||
"%-6s %-58s %s", "Where", "Path", "Size"));
|
||||
for (const ImageTextureMemEntry &e : entriesCopy) {
|
||||
std::string whereTag = StringPrintf("[%s]", e.where);
|
||||
toPrint["Image textures (by path)"].push_back(StringPrintf(
|
||||
"%-6s %-58s %s", whereTag.c_str(), e.path.c_str(), printBytes(e.bytes)));
|
||||
}
|
||||
}
|
||||
for (auto &percentage : stats->percentages) {
|
||||
if (percentage.second.second == 0)
|
||||
continue;
|
||||
int64_t num = percentage.second.first;
|
||||
int64_t denom = percentage.second.second;
|
||||
std::string category, title;
|
||||
getCategoryAndTitle(percentage.first, &category, &title);
|
||||
toPrint[category].push_back(
|
||||
StringPrintf("%-42s%12" PRIu64 " / %12" PRIu64 " (%.2f%%)", title, num, denom,
|
||||
(100.f * num) / denom));
|
||||
}
|
||||
for (auto &ratio : stats->ratios) {
|
||||
if (ratio.second.second == 0)
|
||||
continue;
|
||||
int64_t num = ratio.second.first;
|
||||
int64_t denom = ratio.second.second;
|
||||
std::string category, title;
|
||||
getCategoryAndTitle(ratio.first, &category, &title);
|
||||
toPrint[category].push_back(
|
||||
StringPrintf("%-42s%12" PRIu64 " / %12" PRIu64 " (%.2fx)", title, num, denom,
|
||||
(double)num / (double)denom));
|
||||
|
||||
int64_t rss = GetCurrentRSS();
|
||||
toPrint["Process"].push_back(StringPrintf(
|
||||
"%-42s %s", "RSS (current)", printBytes(size_t(std::max<int64_t>(rss, 0)))));
|
||||
|
||||
if (haveStatsRenderWallSeconds) {
|
||||
toPrint["Timing"].push_back(StringPrintf(
|
||||
"%-42s %10.3f s", "Wall-clock render time", statsRenderWallSeconds));
|
||||
}
|
||||
|
||||
for (auto &categories : toPrint) {
|
||||
|
|
@ -509,6 +518,7 @@ void StatsAccumulator::Clear() {
|
|||
stats->floatDistributions.clear();
|
||||
stats->percentages.clear();
|
||||
stats->ratios.clear();
|
||||
haveStatsRenderWallSeconds = false;
|
||||
}
|
||||
|
||||
} // namespace pbrt
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ void StatsReportPixelStart(Point2i p);
|
|||
void StatsReportPixelEnd(Point2i p);
|
||||
|
||||
void PrintStats(FILE *dest);
|
||||
// Wall-clock seconds for the RenderCPU / RenderWavefront call; shown under --stats timing.
|
||||
void SetStatsRenderWallSeconds(Float seconds);
|
||||
// Per loaded imagemap: _where_ is "CPU" (host MIPMap pyramids) or "GPU" (device mip arrays).
|
||||
void ReportImageTextureMemory(const std::string &path, int64_t bytes, const char *where);
|
||||
void StatsWritePixelImages();
|
||||
bool PrintCheckRare(FILE *dest);
|
||||
void ClearStats();
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include <pbrt/util/spectrum.h>
|
||||
#include <pbrt/util/stats.h>
|
||||
#include <pbrt/util/string.h>
|
||||
#include <pbrt/texture_mip_preprocess.h>
|
||||
#include <pbrt/util/taggedptr.h>
|
||||
#include <pbrt/wavefront/aggregate.h>
|
||||
|
||||
|
|
@ -110,6 +111,12 @@ WavefrontPathIntegrator::WavefrontPathIntegrator(
|
|||
haveMedia = true;
|
||||
}
|
||||
|
||||
camera = scene.GetCamera();
|
||||
sampler = scene.GetSampler();
|
||||
|
||||
LOG_VERBOSE("Image texture mip preprocess");
|
||||
RunImageTextureMipPreprocess(scene, camera);
|
||||
|
||||
// Textures
|
||||
LOG_VERBOSE("Starting to create textures");
|
||||
NamedTextures textures = scene.CreateTextures();
|
||||
|
|
@ -149,10 +156,8 @@ WavefrontPathIntegrator::WavefrontPathIntegrator(
|
|||
// Retrieve these here so that the CPU isn't writing to managed memory
|
||||
// concurrently with the OptiX acceleration-structure construction work
|
||||
// that follows. (Verbotten on Windows.)
|
||||
camera = scene.GetCamera();
|
||||
film = camera.GetFilm();
|
||||
filter = film.GetFilter();
|
||||
sampler = scene.GetSampler();
|
||||
|
||||
if (Options->useGPU) {
|
||||
#ifdef PBRT_BUILD_GPU_RENDERER
|
||||
|
|
|
|||
|
|
@ -49,9 +49,6 @@ void RenderWavefront(BasicScene &scene) {
|
|||
if (Options->useGPU)
|
||||
ReportKernelStats();
|
||||
#endif // PBRT_BUILD_GPU_RENDERER
|
||||
|
||||
Printf("Wavefront integrator statistics:\n");
|
||||
Printf("%s\n", integrator->stats->Print());
|
||||
}
|
||||
|
||||
#ifdef PBRT_BUILD_GPU_RENDERER
|
||||
|
|
|
|||