This commit is contained in:
alexjwyso 2026-05-14 13:55:18 +01:00 • committed by GitHub
commit 556aac95fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 2133 additions and 165 deletions

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ src/build
.ipynb_checkpoints/
*build*/
.cache/
compare-skipmip-logs/

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

View file

@ -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}")

View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

BIN
scenes/book-skipmip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

BIN
scenes/book.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

BIN
scenes/quick_preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

BIN
scenes/sanmiguel-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
scenes/sanmiguel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

View file

@ -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;

View file

@ -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

View file

@ -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");

View file

@ -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];

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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);

View file

@ -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 &parameters);
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;

View 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

View 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

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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];

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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();

View file

@ -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

View file

@ -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