Skip to content

Env map importance sampling#969

Open
kevyuu wants to merge 55 commits intomasterfrom
env_map_importance_sampling
Open

Env map importance sampling#969
kevyuu wants to merge 55 commits intomasterfrom
env_map_importance_sampling

Conversation

@kevyuu
Copy link
Contributor

@kevyuu kevyuu commented Dec 19, 2025

Description

Rework environment map importance sampling to vulkan and hlsl

Testing

Rework example 0 to use vulkan and hlsl
Unit Test Pull Request

TODO list:

  • Implement Warpmap Generation in hlsl
  • Implement Warpmap hierarchical map sampling in hlsl

Comment on lines +55 to +60
vector2_type binarySearch(const uint32_t2 coord)
{
// We use _lastWarpPixel here for corner sampling
float32_t2 xi = float32_t2(coord)/ _lastWarpPixel;
uint32_t2 p = uint32_t2(0, 0);
const uint32_t2 mip2x1 = findMSB(_mapSize.y);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be able to use this as a sampler, I also need a codomain_type generate(const domaint_type xi) so best you construct this binarySearch in terms of that

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you also wouldn't store _lastWarpPixel here then, because it would just be a warpmap agnostic sampler

Comment on lines +14 to +35
// declare concept
#define NBL_CONCEPT_NAME LuminanceReadAccessor
#define NBL_CONCEPT_TPLT_PRM_KINDS (typename)(typename)
#define NBL_CONCEPT_TPLT_PRM_NAMES (U)(ScalarT)
// not the greatest syntax but works
#define NBL_CONCEPT_PARAM_0 (a,U)
#define NBL_CONCEPT_PARAM_1 (coord,uint32_t2)
#define NBL_CONCEPT_PARAM_2 (level,uint32_t)
// start concept
NBL_CONCEPT_BEGIN(3)
// need to be defined AFTER the concept begins
#define a NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_0
#define coord NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_1
#define level NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_2
NBL_CONCEPT_END(
((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((a.template texelFetch(coord,level)) , ::nbl::hlsl::is_same_v, ScalarT))
((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((a.template texelGather(coord,level)) , ::nbl::hlsl::is_same_v, vector<ScalarT, 4>))
);
#undef level
#undef coord
#undef a
#include <nbl/builtin/hlsl/concepts/__end.hlsl>
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you merge latest path tracer branch, you can forego this concept in lieu of a LoadableImage with Components=1 and add a GatherableImage with all the other accessors , see #969 (comment)

Comment on lines +112 to +187
template <typename ScalarT, typename LuminanceAccessorT, typename HierarchicalSamplerT, typename PostWarpT
NBL_PRIMARY_REQUIRES(is_scalar_v<ScalarT> &&
concepts::accessors::GenericReadAccessor<LuminanceAccessorT, ScalarT, float32_t2> &&
hierarchical_image::HierarchicalSampler<HierarchicalSamplerT, ScalarT> &&
concepts::Warp<PostWarpT>)
struct HierarchicalImage
{
using scalar_type = ScalarT;
using vector2_type = vector<ScalarT, 2>;
using vector3_type = vector<ScalarT, 3>;
using vector4_type = vector<ScalarT, 4>;
LuminanceAccessorT _lumaMap;
HierarchicalSamplerT _warpMap;
uint32_t2 _warpSize;
uint32_t2 _lastWarpPixel;
scalar_type _rcpAvgLuma;

static HierarchicalImage create(NBL_CONST_REF_ARG(LuminanceAccessorT) lumaMap, NBL_CONST_REF_ARG(HierarchicalSamplerT) warpMap, uint32_t2 warpSize, scalar_type avgLuma)
{
HierarchicalImage<ScalarT, LuminanceAccessorT, HierarchicalSamplerT, PostWarpT> result;
result._lumaMap = lumaMap;
result._warpMap = warpMap;
result._warpSize = warpSize;
result._lastWarpPixel = warpSize - uint32_t2(1, 1);
result._rcpAvgLuma = ScalarT(1.0) / avgLuma;
return result;
}

vector2_type inverseWarp_and_deferredPdf(NBL_REF_ARG(scalar_type) pdf, vector3_type direction) NBL_CONST_MEMBER_FUNC
{
vector2_type envmapUv = PostWarpT::inverseWarp(direction);
scalar_type luma;
_lumaMap.get(envmapUv, luma);
pdf = (luma * _rcpAvgLuma) * PostWarpT::backwardDensity(direction);
return envmapUv;
}

scalar_type deferredPdf(vector3_type direction) NBL_CONST_MEMBER_FUNC
{
vector2_type envmapUv = PostWarpT::inverseWarp(direction);
scalar_type luma;
_lumaMap.get(envmapUv, luma);
return luma * _rcpAvgLuma * PostWarpT::backwardDensity(direction);
}

vector3_type generate_and_pdf(NBL_REF_ARG(scalar_type) pdf, NBL_REF_ARG(vector2_type) uv, vector2_type xi) NBL_CONST_MEMBER_FUNC
{
const vector2_type texelCoord = xi * float32_t2(_lastWarpPixel);

matrix<scalar_type, 4, 2> uvs = _warpMap.sampleUvs(uint32_t2(texelCoord));

const vector2_type interpolant = frac(texelCoord);

const vector2_type xDiffs[] = {
uvs[2] - uvs[3],
uvs[1] - uvs[0]
};
const vector2_type yVals[] = {
xDiffs[0] * interpolant.x + uvs[3],
xDiffs[1] * interpolant.x + uvs[0]
};
const vector2_type yDiff = yVals[1] - yVals[0];
uv = yDiff * interpolant.y + yVals[0];

const WarpResult<vector3_type> warpResult = PostWarpT::warp(uv);

const scalar_type detInterpolJacobian = determinant(matrix<scalar_type, 2, 2>(
lerp(xDiffs[0], xDiffs[1], interpolant.y), // first column dFdx
yDiff // second column dFdy
)) * _lastWarpPixel.x * _lastWarpPixel.y;

pdf = abs(warpResult.density / detInterpolJacobian);

return warpResult.dst;
}
};
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate file, also struct needs better name like WarpMap sampler

#undef a
#include <nbl/builtin/hlsl/concepts/__end.hlsl>

// sampleUvs return 4 UVs in a square to calculate the jacobian matrix

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace "to calculate the jacobian matrix" with "for manual bilinear interpolation with differentiability"

struct SLumaGenPushConstants
{
float32_t3 lumaRGBCoefficients;
uint32_t2 lumaMapResolution;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use uint32_t bitfield of 16 bit each per axis (don't use uint16_t2 though because AMD doesn't support 16 bit push constants)

[[vk::binding(0, 0)]] Texture2D<float32_t4> envMap;
[[vk::binding(1, 0)]] RWTexture2D<float32_t> outImage;

[numthreads(WORKGROUP_DIM, WORKGROUP_DIM, 1)]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho don't pass a define, and hardcode to 16x16 (but obviously leave a cosntexpr in a shared header so the c++ can be in-sync)


const float uv_y = (float(threadID.y) + float(0.5f)) / pc.lumaMapResolution.y;
const float32_t3 envMapSample = envMap.Load(float32_t3(threadID.xy, 0));
const float32_t luma = hlsl::dot(envMapSample, pc.lumaRGBCoefficients) * sin(numbers::pi<float32_t> * uv_y);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this assumes a spherical warp

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generalize

Comment on lines +9 to +10
[[vk::binding(0, 0)]] Texture2D<float32_t4> envMap;
[[vk::binding(1, 0)]] RWTexture2D<float32_t> outImage;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this assumes a 2D map and not a cubemap/layered thing, use layered images instead

Comment on lines +20 to +25
return float32_t4(
lumaMap.Load(uint32_t3(coord, level), uint32_t2(0, 1)),
lumaMap.Load(uint32_t3(coord, level), uint32_t2(1, 1)),
lumaMap.Load(uint32_t3(coord, level), uint32_t2(1, 0)),
lumaMap.Load(uint32_t3(coord, level), uint32_t2(0, 0))
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw OOB reads using texelFetch are undefined, asser that coord is between 0 and lastPixel-1 for both axes

#define NBL_CONCEPT_TPLT_PRM_KINDS (typename)(typename)
#define NBL_CONCEPT_TPLT_PRM_NAMES (HierarchicalSamplerT)(ScalarT)
// not the greatest syntax but works
#define NBL_CONCEPT_PARAM_0 (sampler,HierarchicalSamplerT)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sampler is a reserved keyword in HLSL

#define sampler NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_0
#define coord NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_1
NBL_CONCEPT_END(
((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((sampler.template sampleUvs(coord)) , ::nbl::hlsl::is_same_v, matrix<ScalarT, 4, 2>))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signature should be template sampleUvs<matrix<>,vector<>>(outMatrix,coord)

Comment on lines +41 to +42
LuminanceSampler luminanceSampler =
LuminanceSampler::create(luminanceAccessor, uint32_t2(lumaMapWidth, lumaMapHeight), lumaMapWidth != lumaMapHeight, uint32_t2(lumaMapWidth, lumaMapHeight));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we passing the uint32_t2(lumaMapWidth, lumaMapHeight) resolution twice ?

Comment on lines +44 to +47
uint32_t2 pixelCoord = threadID.xy;

outImage[pixelCoord] = luminanceSampler.binarySearch(pixelCoord);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to handle OOB, I can have a 4x4 sphere map, but my workgroup is 16x16

Comment on lines +17 to +73
template <typename T = float32_t>
struct Spherical
{
using density_type = T;
using domain_type = vector<density_type, 2>;
using codomain_type = vector<density_type, 3>;

template <typename DomainT NBL_FUNC_REQUIRES(is_same_v<DomainT, domain_type>)
static WarpResult<codomain_type> warp(const DomainT uv)
{
codomain_type dir;
dir.x = cos(uv.x * density_type(2) * numbers::pi<density_type>);
dir.z = sqrt(density_type(1) - (dir.x * dir.x));
if (uv.x > density_type(0.5))
dir.z = -dir.z;
const density_type theta = uv.y * numbers::pi<density_type>;
const density_type cosTheta = cos(theta);
const density_type sinTheta = sqrt(density_type(1) - (cosTheta * cosTheta));
dir.xz *= sinTheta;
dir.y = cosTheta;

WarpResult<codomain_type> warpResult;
warpResult.dst = dir;
warpResult.density = density_type(1) / (density_type(2) * sinTheta * numbers::pi<density_type> * numbers::pi<density_type>);

return warpResult;
}

template <typename CodomainT NBL_FUNC_REQUIRES(is_same_v<CodomainT, codomain_type>)
static domain_type inverseWarp(const CodomainT v)
{
const density_type phi = atan2(v.z, v.x);
const density_type theta = acos(v.y);
density_type uv_x = phi * density_type(0.5) * numbers::inv_pi<density_type>;
if (uv_x < density_type(0))
uv_x += density_type(1);
density_type uv_y = theta * numbers::inv_pi<density_type>;
return domain_type(uv_x, uv_y);
}


template <typename DomainT NBL_FUNC_REQUIRES(is_same_v<DomainT, domain_type>)
static density_type forwardDensity(const DomainT uv)
{
const density_type theta = uv.y * numbers::pi<density_type>;
return density_type(1) / (sin(theta) * density_type(2) * numbers::pi<density_type> * numbers::pi<density_type>);

}

template <typename CodomainT NBL_FUNC_REQUIRES(is_same_v<CodomainT, codomain_type>)
static density_type backwardDensity(const CodomainT dst)
{
const density_type cosTheta = dst.y;
const density_type sinTheta = sqrt(density_type(1) - (cosTheta * cosTheta));
return density_type(1) / (sinTheta * density_type(2) * numbers::pi<density_type> * numbers::pi<density_type>);
}
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

under the framework of #1001 we can make this into a Bijective Sampler

Comment on lines +97 to +109
matrix<scalar_type, 4, 2> sampleUvs(uint32_t2 sampleCoord) NBL_CONST_MEMBER_FUNC
{
const vector2_type dir0 = binarySearch(sampleCoord + vector2_type(0, 1));
const vector2_type dir1 = binarySearch(sampleCoord + vector2_type(1, 1));
const vector2_type dir2 = binarySearch(sampleCoord + vector2_type(1, 0));
const vector2_type dir3 = binarySearch(sampleCoord);
return matrix<scalar_type, 4, 2>(
dir0,
dir1,
dir2,
dir3
);
}
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure that sampleUvs should be exposed like that (if something needs it -like a test- let them call binarySearch 4 times), why?

  1. if not using a warpmap you wouldn't actually importance sample 4 times to perform finite difference and get your jacobian, you'd use the Luma of the image (with NEAREST not linear sampler) as the PDF because thats 100% accurate
  2. if using a warmap you'd textureGather it and work out the Jacobian from differntiating the bilinear interpolation equation

is_scalar_v<ScalarT> &&
hierarchical_image::LuminanceReadAccessor<LuminanceAccessorT, ScalarT>
)
struct LuminanceMapSampler

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is actually the HierarchicalLuminanceSampler

// We use _lastWarpPixel here for corner sampling
float32_t2 xi = float32_t2(coord)/ _lastWarpPixel;
uint32_t2 p = uint32_t2(0, 0);
const uint32_t2 mip2x1 = findMSB(_mapSize.y);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you using a uint32_t2 to store this, its a uint16_t at most, its a scalar quantity

Also should be precomputed

LuminanceAccessorT _map;
uint32_t2 _mapSize;
uint32_t2 _lastWarpPixel;
bool _aspect2x1;
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

store these instead

float32_t2 rcpMapSize;
uint16_t mip2x1 : 15;
uint16_t aspect2x1 : 1;

Comment on lines +20 to +24
template <typename ScalarT, typename LuminanceAccessorT
NBL_PRIMARY_REQUIRES(
is_scalar_v<ScalarT> &&
hierarchical_image::LuminanceReadAccessor<LuminanceAccessorT, ScalarT>
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a bool for whether the accessor is corner sampled or not



// If we don`t add xi, the sample will clump to the lowest corner of environment map texel. We add xi to simulate uniform distribution within a pixel and make the sample continuous. This is why we compute the pdf not from the normalized luminance of the texel, instead from the reciprocal of the Jacobian.
const vector2_type directionUV = (vector2_type(p.x, p.y) + xi) / vector2_type(_mapSize);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if your spheremap or octahedral maps are corner sampled, the edge pixels need special treatment

Essentially whenever your X or Y coordinate is an edge coordinate, you'd need to weight their luma contribution down by 50% and also change how the remaining xi gets added:

  1. coord==0 then xi gets rescaled to [0.5,1.0]
  2. coord==Last then xi gets rescaled to [0,0.5]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then you'd actually rescale that final UV from [0.5/size,1-0.5/size] to [0,1] before outputting it in the warpmap

Comment on lines +1 to +55
#ifndef _NBL_BUILTIN_HLSL_SAMPLING_CONCEPTS_WARP_INCLUDED_
#define _NBL_BUILTIN_HLSL_SAMPLING_CONCEPTS_WARP_INCLUDED_

#include <nbl/builtin/hlsl/concepts.hlsl>

namespace nbl
{
namespace hlsl
{
namespace sampling
{

template <typename CodomainT, typename DensityT = float32_t>
struct WarpResult
{
CodomainT dst;
DensityT density;
};
}

namespace concepts
{

// declare concept
#define NBL_CONCEPT_NAME Warp
#define NBL_CONCEPT_TPLT_PRM_KINDS (typename)
#define NBL_CONCEPT_TPLT_PRM_NAMES (U)
// not the greatest syntax but works
#define NBL_CONCEPT_PARAM_0 (warper,U)
#define NBL_CONCEPT_PARAM_1 (xi,typename U::domain_type)
#define NBL_CONCEPT_PARAM_2 (dst,typename U::codomain_type)
// start concept
NBL_CONCEPT_BEGIN(3)
#define warper NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_0
#define xi NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_1
#define dst NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_2
NBL_CONCEPT_END(
((NBL_CONCEPT_REQ_TYPE)(U::domain_type))
((NBL_CONCEPT_REQ_TYPE)(U::codomain_type))
((NBL_CONCEPT_REQ_TYPE)(U::density_type))
((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((warper.template warp<typename U::domain_type>(xi)) , ::nbl::hlsl::is_same_v, sampling::WarpResult<typename U::codomain_type, typename U::density_type>))
((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((warper.template forwardDensity<typename U::domain_type>(xi)) , ::nbl::hlsl::is_same_v, typename U::density_type))
((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((warper.template backwardDensity<typename U::codomain_type>(dst)) , ::nbl::hlsl::is_same_v, typename U::density_type))
);
#undef dst
#undef xi
#undef warper
#include <nbl/builtin/hlsl/concepts/__end.hlsl>

}

}
}

#endif No newline at end of file

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file will be made obsolete by #1001 s Bijective Sampler concept

Comment on lines +4 to +7
#include "nbl/video/declarations.h"

namespace nbl::core
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

completely wrong namespace, you're using video, this file needs to live in nbl/video/sampling

set(NBL_CORE_SOURCES
core/alloc/refctd_memory_resource.cpp
core/hash/blake.cpp
core/sampling/EnvmapSampler.cpp

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, this should be video

option(NBL_BUILD_MITSUBA_LOADER "Enable nbl::ext::MitsubaLoader?" ON)
option(NBL_BUILD_IMGUI "Enable nbl::ext::ImGui?" ON)
option(NBL_BUILD_DEBUG_DRAW "Enable Nabla Debug Draw extension?" ON)
option(NBL_BUILD_ENVMAP_IMPORTANCE_SAMPLING "Enable Nabla Envmap Importance Sampling extension?" ON)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no longer an extension, dont need the option

Comment on lines +140 to +157
vector2_type inverseWarp_and_deferredPdf(NBL_REF_ARG(scalar_type) pdf, vector3_type direction) NBL_CONST_MEMBER_FUNC
{
vector2_type envmapUv = PostWarpT::inverseWarp(direction);
scalar_type luma;
_lumaMap.get(envmapUv, luma);
pdf = (luma * _rcpAvgLuma) * PostWarpT::backwardDensity(direction);
return envmapUv;
}

scalar_type deferredPdf(vector3_type direction) NBL_CONST_MEMBER_FUNC
{
vector2_type envmapUv = PostWarpT::inverseWarp(direction);
scalar_type luma;
_lumaMap.get(envmapUv, luma);
return luma * _rcpAvgLuma * PostWarpT::backwardDensity(direction);
}

vector3_type generate_and_pdf(NBL_REF_ARG(scalar_type) pdf, NBL_REF_ARG(vector2_type) uv, vector2_type xi) NBL_CONST_MEMBER_FUNC

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it conform to #1001 's ResamplableSampler

Comment on lines +112 to +117
template <typename ScalarT, typename LuminanceAccessorT, typename HierarchicalSamplerT, typename PostWarpT
NBL_PRIMARY_REQUIRES(is_scalar_v<ScalarT> &&
concepts::accessors::GenericReadAccessor<LuminanceAccessorT, ScalarT, float32_t2> &&
hierarchical_image::HierarchicalSampler<HierarchicalSamplerT, ScalarT> &&
concepts::Warp<PostWarpT>)
struct HierarchicalImage

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be called WarpmapSampler or something like that

const vector2_type texelCoord = xi * float32_t2(_lastWarpPixel);

matrix<scalar_type, 4, 2> uvs = _warpMap.sampleUvs(uint32_t2(texelCoord));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warpmap should convert you from [0,1] normalized xi to its own texels,the sampleUvs should really work same as textureGather, it should take normalized uvs as input


matrix<scalar_type, 4, 2> uvs = _warpMap.sampleUvs(uint32_t2(texelCoord));

const vector2_type interpolant = frac(texelCoord);
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make the _warpMap.gather spit out the interpolant so its cleaner (requires a specialized gather overload, adding on top of Gatherable concept)

LuminanceAccessorT _lumaMap;
HierarchicalSamplerT _warpMap;
uint32_t2 _warpSize;
uint32_t2 _lastWarpPixel;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you actually only need to store the _lastWarpPixel.x * _lastWarpPixel.y prodct, I expect the warpmap to handle gathering

Comment on lines +87 to +88
if (choseSecond(wx_0, wx_1, xi.x))
p.x |= 1;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw you can get the PDF of the finally chosen texel as metadata if on the final call to choseSecond you save wx_0 or wx_1 and thats your chosen pixel luminance, you just need to know the average to and total number of texels (corner sampled edges count as half pixels) get the PDF

(obvioulsy if mip2x1 is 0, the PDF is either 1.0 or the dummy from the choice on line 64)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants