Draw: Use VectorSet for generic attribute requests

Replace `DRW_Attributes` with a VectorSet of std::string. The max number of
attributes is still the same. The inline buffer size is 4, and std::string's inline
buffer is smaller than the previous char array size of 64, but it seems
reasonable to save those optimizations for shorter attribute names and
fewer attributes. In return we significantly decrease the size of the batch
caches, simplify the code, and remove the attribute name length limit.

I observed roughly an 8% increase in the 30k cube objects file, a change from
12 to 13 FPS. I'm guessing this is mostly because `VectorSet<std::string>` is
smaller than `DRW_Attributes`.

Pull Request: https://projects.blender.org/blender/blender/pulls/138946
This commit is contained in:
Hans Goudey
2025-05-15 20:43:31 +02:00
committed by Hans Goudey
parent a3748b549b
commit 760cf70d63
11 changed files with 83 additions and 127 deletions

View File

@@ -2,62 +2,34 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_string.h"
#include "BKE_customdata.hh"
#include "GPU_shader.hh"
#include "draw_attributes.hh"
namespace blender::draw {
/* Return true if the given DRW_AttributeRequest is already in the requests. */
static bool drw_attributes_has_request(const DRW_Attributes *requests,
const DRW_AttributeRequest &req)
{
for (int i = 0; i < requests->num_requests; i++) {
const DRW_AttributeRequest &src_req = requests->requests[i];
if (STREQ(src_req.attribute_name, req.attribute_name)) {
return true;
}
}
return false;
}
static void drw_attributes_merge_requests(const DRW_Attributes *src_requests,
DRW_Attributes *dst_requests)
{
for (int i = 0; i < src_requests->num_requests; i++) {
if (dst_requests->num_requests == GPU_MAX_ATTR) {
return;
}
if (drw_attributes_has_request(dst_requests, src_requests->requests[i])) {
continue;
}
dst_requests->requests[dst_requests->num_requests] = src_requests->requests[i];
dst_requests->num_requests += 1;
}
}
void drw_attributes_clear(DRW_Attributes *attributes)
void drw_attributes_clear(VectorSet<std::string> *attributes)
{
*attributes = {};
}
void drw_attributes_merge(DRW_Attributes *dst, const DRW_Attributes *src, Mutex &render_mutex)
void drw_attributes_merge(VectorSet<std::string> *dst,
const VectorSet<std::string> *src,
Mutex &render_mutex)
{
if (src->num_requests == 0) {
if (src->is_empty()) {
return;
}
std::lock_guard lock{render_mutex};
drw_attributes_merge_requests(src, dst);
dst->add_multiple(src->as_span());
}
bool drw_attributes_overlap(const DRW_Attributes *a, const DRW_Attributes *b)
bool drw_attributes_overlap(const VectorSet<std::string> *a, const VectorSet<std::string> *b)
{
for (int i = 0; i < b->num_requests; i++) {
if (!drw_attributes_has_request(a, b->requests[i])) {
for (const std::string &req : b->as_span()) {
if (!a->contains(req)) {
return false;
}
}
@@ -65,20 +37,16 @@ bool drw_attributes_overlap(const DRW_Attributes *a, const DRW_Attributes *b)
return true;
}
void drw_attributes_add_request(DRW_Attributes *attrs, const char *name)
void drw_attributes_add_request(VectorSet<std::string> *attrs, const StringRef name)
{
DRW_AttributeRequest req{};
STRNCPY(req.attribute_name, name);
if (attrs->num_requests >= GPU_MAX_ATTR || drw_attributes_has_request(attrs, req)) {
if (attrs->size() >= GPU_MAX_ATTR) {
return;
}
attrs->requests[attrs->num_requests] = req;
attrs->num_requests += 1;
attrs->add_as(name);
}
bool drw_custom_data_match_attribute(const CustomData &custom_data,
const char *name,
const StringRef name,
int *r_layer_index,
eCustomDataType *r_type)
{

View File

@@ -10,12 +10,15 @@
#pragma once
#include <string>
#include "DNA_customdata_types.h"
#include "BLI_mutex.hh"
#include "BLI_string_ref.hh"
#include "BLI_sys_types.h"
#include "GPU_shader.hh"
#include "BLI_vector_set.hh"
namespace blender::bke {
enum class AttrDomain : int8_t;
@@ -23,15 +26,6 @@ enum class AttrDomain : int8_t;
namespace blender::draw {
struct DRW_AttributeRequest {
char attribute_name[64];
};
struct DRW_Attributes {
DRW_AttributeRequest requests[GPU_MAX_ATTR];
int num_requests;
};
struct DRW_MeshCDMask {
uint32_t uv : 8;
uint32_t tan : 8;
@@ -49,17 +43,19 @@ struct DRW_MeshCDMask {
* See `mesh_cd_layers_type_*` functions. */
static_assert(sizeof(DRW_MeshCDMask) <= sizeof(uint32_t), "DRW_MeshCDMask exceeds 32 bits");
void drw_attributes_clear(DRW_Attributes *attributes);
void drw_attributes_clear(VectorSet<std::string> *attributes);
void drw_attributes_merge(DRW_Attributes *dst, const DRW_Attributes *src, Mutex &render_mutex);
void drw_attributes_merge(VectorSet<std::string> *dst,
const VectorSet<std::string> *src,
Mutex &render_mutex);
/* Return true if all requests in b are in a. */
bool drw_attributes_overlap(const DRW_Attributes *a, const DRW_Attributes *b);
bool drw_attributes_overlap(const VectorSet<std::string> *a, const VectorSet<std::string> *b);
void drw_attributes_add_request(DRW_Attributes *attrs, const char *name);
void drw_attributes_add_request(VectorSet<std::string> *attrs, StringRef name);
bool drw_custom_data_match_attribute(const CustomData &custom_data,
const char *name,
StringRef name,
int *r_layer_index,
eCustomDataType *r_type);

View File

@@ -295,20 +295,20 @@ struct MeshBatchCache {
DRW_MeshCDMask cd_used, cd_needed, cd_used_over_time;
DRW_Attributes attr_used, attr_needed, attr_used_over_time;
VectorSet<std::string> attr_used, attr_needed, attr_used_over_time;
int lastmatch;
/* Valid only if edge_detection is up to date. */
bool is_manifold;
bool no_loose_wire;
/* Total areas for drawing UV Stretching. Contains the summed area in mesh
* space (`tot_area`) and the summed area in uv space (`tot_uvarea`).
*
* Only valid after `DRW_mesh_batch_cache_create_requested` has been called. */
float tot_area, tot_uv_area;
bool no_loose_wire;
};
#define MBC_EDITUV \

View File

@@ -266,8 +266,7 @@ void mesh_buffer_cache_create_requested(TaskGraph & /*task_graph*/,
case VBOType::Attr14:
case VBOType::Attr15: {
const int8_t attr_index = int8_t(vbos_to_create[i]) - int8_t(VBOType::Attr0);
created_vbos[i] = extract_attribute(mr,
cache.attr_used.requests[attr_index].attribute_name);
created_vbos[i] = extract_attribute(mr, cache.attr_used[attr_index]);
break;
}
case VBOType::AttrViewer:
@@ -451,9 +450,8 @@ void mesh_buffer_cache_create_requested_subdiv(MeshBatchCache &cache,
for (const int8_t i : IndexRange(GPU_MAX_ATTR)) {
const VBOType request = VBOType(int8_t(VBOType::Attr0) + i);
if (vbos_to_create.contains(request)) {
buffers.vbos.add_new(
request,
extract_attribute_subdiv(mr, subdiv_cache, cache.attr_used.requests[i].attribute_name));
buffers.vbos.add_new(request,
extract_attribute_subdiv(mr, subdiv_cache, cache.attr_used[i]));
}
}
}

View File

@@ -741,11 +741,10 @@ static bool ensure_attributes(const Curves &curves,
if (gpu_material) {
/* The following code should be kept in sync with `mesh_cd_calc_used_gpu_layers`. */
DRW_Attributes attrs_needed;
drw_attributes_clear(&attrs_needed);
VectorSet<std::string> attrs_needed;
ListBase gpu_attrs = GPU_material_attributes(gpu_material);
LISTBASE_FOREACH (const GPUMaterialAttribute *, gpu_attr, &gpu_attrs) {
const char *name = gpu_attr->name;
StringRef name = gpu_attr->name;
eCustomDataType type = static_cast<eCustomDataType>(gpu_attr->type);
int layer = -1;
std::optional<bke::AttrDomain> domain;
@@ -755,7 +754,7 @@ static bool ensure_attributes(const Curves &curves,
*
* We do it based on the specified name.
*/
if (name[0] != '\0') {
if (!name.is_empty()) {
layer = CustomData_get_named_layer(&cd_curve, CD_PROP_FLOAT2, name);
type = CD_MTFACE;
domain = bke::AttrDomain::Curve;
@@ -861,14 +860,12 @@ static bool ensure_attributes(const Curves &curves,
bool need_tf_update = false;
for (const int i : IndexRange(final_cache.attr_used.num_requests)) {
const DRW_AttributeRequest &request = final_cache.attr_used.requests[i];
for (const int i : final_cache.attr_used.index_range()) {
if (cache.eval_cache.final.attributes_buf[i] != nullptr) {
continue;
}
ensure_final_attribute(curves, request.attribute_name, i, cache.eval_cache);
ensure_final_attribute(curves, final_cache.attr_used[i], i, cache.eval_cache);
if (cache.eval_cache.proc_attributes_point_domain[i]) {
need_tf_update = true;
}
@@ -882,7 +879,7 @@ static void request_attribute(Curves &curves, const char *name)
CurvesBatchCache &cache = get_batch_cache(curves);
CurvesEvalFinalCache &final_cache = cache.eval_cache.final;
DRW_Attributes attributes{};
VectorSet<std::string> attributes{};
bke::CurvesGeometry &curves_geometry = curves.geometry.wrap();
if (!curves_geometry.attributes().contains(name)) {
@@ -1045,8 +1042,8 @@ gpu::VertBuf **DRW_curves_texture_for_evaluated_attribute(Curves *curves,
request_attribute(*curves, name);
int request_i = -1;
for (const int i : IndexRange(final_cache.attr_used.num_requests)) {
if (STREQ(final_cache.attr_used.requests[i].attribute_name, name)) {
for (const int i : final_cache.attr_used.index_range()) {
if (final_cache.attr_used[i] == name) {
request_i = i;
break;
}

View File

@@ -179,7 +179,7 @@ static void mesh_cd_calc_active_mask_uv_layer(const Object &object,
static DRW_MeshCDMask mesh_cd_calc_used_gpu_layers(const Object &object,
const Mesh &mesh,
const Span<const GPUMaterial *> materials,
DRW_Attributes *attributes)
VectorSet<std::string> *attributes)
{
const Mesh &me_final = editmesh_final_or_this(object, mesh);
const CustomData &cd_ldata = mesh_cd_ldata_get_from_mesh(me_final);
@@ -683,7 +683,7 @@ static void texpaint_request_active_uv(MeshBatchCache &cache, Object &object, Me
static void request_active_and_default_color_attributes(const Object &object,
const Mesh &mesh,
DRW_Attributes &attributes)
VectorSet<std::string> &attributes)
{
const Mesh &me_final = editmesh_final_or_this(object, mesh);
const CustomData &cd_vdata = mesh_cd_vdata_get_from_mesh(me_final);
@@ -774,11 +774,10 @@ gpu::Batch *DRW_mesh_batch_cache_get_edit_mesh_analysis(Mesh &mesh)
void DRW_mesh_get_attributes(const Object &object,
const Mesh &mesh,
const Span<const GPUMaterial *> materials,
DRW_Attributes *r_attrs,
VectorSet<std::string> *r_attrs,
DRW_MeshCDMask *r_cd_needed)
{
DRW_Attributes attrs_needed;
drw_attributes_clear(&attrs_needed);
VectorSet<std::string> attrs_needed;
DRW_MeshCDMask cd_needed = mesh_cd_calc_used_gpu_layers(object, mesh, materials, &attrs_needed);
if (r_attrs) {
@@ -794,8 +793,7 @@ Span<gpu::Batch *> DRW_mesh_batch_cache_get_surface_shaded(
Object &object, Mesh &mesh, const Span<const GPUMaterial *> materials)
{
MeshBatchCache &cache = *mesh_batch_cache_get(mesh);
DRW_Attributes attrs_needed;
drw_attributes_clear(&attrs_needed);
VectorSet<std::string> attrs_needed;
DRW_MeshCDMask cd_needed = mesh_cd_calc_used_gpu_layers(object, mesh, materials, &attrs_needed);
BLI_assert(materials.size() == cache.mat_len);
@@ -826,7 +824,7 @@ gpu::Batch *DRW_mesh_batch_cache_get_surface_vertpaint(Object &object, Mesh &mes
{
MeshBatchCache &cache = *mesh_batch_cache_get(mesh);
DRW_Attributes attrs_needed{};
VectorSet<std::string> attrs_needed{};
request_active_and_default_color_attributes(object, mesh, attrs_needed);
drw_attributes_merge(&cache.attr_needed, &attrs_needed, mesh.runtime->render_mutex);
@@ -839,7 +837,7 @@ gpu::Batch *DRW_mesh_batch_cache_get_surface_sculpt(Object &object, Mesh &mesh)
{
MeshBatchCache &cache = *mesh_batch_cache_get(mesh);
DRW_Attributes attrs_needed{};
VectorSet<std::string> attrs_needed{};
request_active_and_default_color_attributes(object, mesh, attrs_needed);
drw_attributes_merge(&cache.attr_needed, &attrs_needed, mesh.runtime->render_mutex);
@@ -1311,7 +1309,7 @@ void DRW_mesh_batch_cache_create_requested(TaskGraph &task_graph,
if (cache.cd_used.uv != 0) {
batch.vbos.append(VBOType::UVs);
}
for (const int i : IndexRange(cache.attr_used.num_requests)) {
for (const int i : cache.attr_used.index_range()) {
batch.vbos.append(VBOType(int8_t(VBOType::Attr0) + i));
}
batch_info.append(std::move(batch));
@@ -1654,7 +1652,7 @@ void DRW_mesh_batch_cache_create_requested(TaskGraph &task_graph,
ibo_requests[int(BufferList::Final)].add(IBOType::Tris);
vbo_requests[int(BufferList::Final)].add(VBOType::CornerNormal);
vbo_requests[int(BufferList::Final)].add(VBOType::Position);
for (const int i : IndexRange(cache.attr_used.num_requests)) {
for (const int i : cache.attr_used.index_range()) {
vbo_requests[int(BufferList::Final)].add(VBOType(int8_t(VBOType::Attr0) + i));
}
if (cache.cd_used.uv != 0) {
@@ -1765,7 +1763,7 @@ void DRW_mesh_batch_cache_create_requested(TaskGraph &task_graph,
if (cache.cd_used.orco != 0) {
GPU_batch_vertbuf_add(batch, buffers.vbos.lookup(VBOType::Orco).get(), false);
}
for (const int i : IndexRange(cache.attr_used.num_requests)) {
for (const int i : cache.attr_used.index_range()) {
GPU_batch_vertbuf_add(
batch, buffers.vbos.lookup(VBOType(int8_t(VBOType::Attr0) + i)).get(), false);
}

View File

@@ -59,12 +59,12 @@ struct PointCloudEvalCache {
gpu::VertBuf *attributes_buf[GPU_MAX_ATTR];
/** Attributes currently being drawn or about to be drawn. */
DRW_Attributes attr_used;
VectorSet<std::string> attr_used;
/**
* Attributes that were used at some point. This is used for garbage collection, to remove
* attributes that are not used in shaders anymore due to user edits.
*/
DRW_Attributes attr_used_over_time;
VectorSet<std::string> attr_used_over_time;
/**
* The last time in seconds that the `attr_used` and `attr_used_over_time` were exactly the same.
@@ -303,7 +303,7 @@ static void pointcloud_extract_position_and_radius(const PointCloud &pointcloud,
static void pointcloud_extract_attribute(const PointCloud &pointcloud,
PointCloudBatchCache &cache,
const DRW_AttributeRequest &request,
const StringRef name,
int index)
{
gpu::VertBuf &attr_buf = *cache.eval_cache.attributes_buf[index];
@@ -316,7 +316,7 @@ static void pointcloud_extract_attribute(const PointCloud &pointcloud,
* similar texture state swizzle to map the attribute correctly as for volume attributes, so we
* can control the conversion ourselves. */
bke::AttributeReader<ColorGeometry4f> attribute = attributes.lookup_or_default<ColorGeometry4f>(
request.attribute_name, bke::AttrDomain::Point, {0.0f, 0.0f, 0.0f, 1.0f});
name, bke::AttrDomain::Point, {0.0f, 0.0f, 0.0f, 1.0f});
static const GPUVertFormat format = [&]() {
GPUVertFormat format{};
@@ -349,13 +349,12 @@ gpu::Batch **pointcloud_surface_shaded_get(PointCloud *pointcloud,
{
const bke::AttributeAccessor attributes = pointcloud->attributes();
PointCloudBatchCache *cache = pointcloud_batch_cache_get(*pointcloud);
DRW_Attributes attrs_needed;
drw_attributes_clear(&attrs_needed);
VectorSet<std::string> attrs_needed;
for (GPUMaterial *gpu_material : Span<GPUMaterial *>(gpu_materials, mat_len)) {
ListBase gpu_attrs = GPU_material_attributes(gpu_material);
LISTBASE_FOREACH (GPUMaterialAttribute *, gpu_attr, &gpu_attrs) {
const char *name = gpu_attr->name;
const StringRef name = gpu_attr->name;
if (!attributes.contains(name)) {
continue;
}
@@ -410,14 +409,14 @@ gpu::VertBuf **DRW_pointcloud_evaluated_attribute(PointCloud *pointcloud, const
return nullptr;
}
{
DRW_Attributes requests{};
VectorSet<std::string> requests{};
drw_attributes_add_request(&requests, name);
drw_attributes_merge(&cache.eval_cache.attr_used, &requests, cache.render_mutex);
}
int request_i = -1;
for (const int i : IndexRange(cache.eval_cache.attr_used.num_requests)) {
if (STREQ(cache.eval_cache.attr_used.requests[i].attribute_name, name)) {
for (const int i : IndexRange(cache.eval_cache.attr_used.index_range())) {
if (cache.eval_cache.attr_used[i] == name) {
request_i = i;
break;
}
@@ -474,11 +473,11 @@ void DRW_pointcloud_batch_cache_create_requested(Object *ob)
DRW_ibo_request(cache.eval_cache.surface_per_mat[i], &cache.eval_cache.geom_indices);
}
}
for (int j = 0; j < cache.eval_cache.attr_used.num_requests; j++) {
for (const int j : cache.eval_cache.attr_used.index_range()) {
DRW_vbo_request(nullptr, &cache.eval_cache.attributes_buf[j]);
if (DRW_vbo_requested(cache.eval_cache.attributes_buf[j])) {
pointcloud_extract_attribute(pointcloud, cache, cache.eval_cache.attr_used.requests[j], j);
pointcloud_extract_attribute(pointcloud, cache, cache.eval_cache.attr_used[j], j);
}
}

View File

@@ -15,6 +15,7 @@
#include "BLI_task.h"
#include "BLI_threads.h"
#include "BLI_vector_set.hh"
#include "GPU_batch.hh"
#include "GPU_context.hh"
@@ -32,7 +33,6 @@ namespace blender::draw {
struct CurvesModule;
struct VolumeModule;
struct PointCloudModule;
struct DRW_Attributes;
struct DRW_MeshCDMask;
class CurveRefinePass;
class View;
@@ -88,7 +88,7 @@ namespace blender::draw {
void DRW_mesh_get_attributes(const Object &object,
const Mesh &mesh,
Span<const GPUMaterial *> materials,
DRW_Attributes *r_attrs,
VectorSet<std::string> *r_attrs,
DRW_MeshCDMask *r_cd_needed);
} // namespace blender::draw

View File

@@ -122,8 +122,8 @@ static void drw_curves_cache_update_compute(CurvesEvalCache *cache)
drw_curves_cache_update_compute(cache, curves_num, cache->final.proc_buf, cache->proc_point_buf);
const DRW_Attributes &attrs = cache->final.attr_used;
for (int i = 0; i < attrs.num_requests; i++) {
const VectorSet<std::string> &attrs = cache->final.attr_used;
for (const int i : attrs.index_range()) {
if (!cache->proc_attributes_point_domain[i]) {
continue;
}
@@ -161,7 +161,7 @@ gpu::VertBuf *DRW_curves_pos_buffer_get(Object *object)
return cache->final.proc_buf;
}
static int attribute_index_in_material(GPUMaterial *gpu_material, const char *name)
static int attribute_index_in_material(GPUMaterial *gpu_material, const StringRef name)
{
if (!gpu_material) {
return -1;
@@ -171,7 +171,7 @@ static int attribute_index_in_material(GPUMaterial *gpu_material, const char *na
ListBase gpu_attrs = GPU_material_attributes(gpu_material);
LISTBASE_FOREACH (GPUMaterialAttribute *, gpu_attr, &gpu_attrs) {
if (STREQ(gpu_attr->name, name)) {
if (gpu_attr->name == name) {
return index;
}
@@ -243,8 +243,8 @@ static CurvesEvalCache *curves_cache_get(Curves &curves,
if (final_points_len > 0) {
cache_update(cache->final.proc_buf, cache->proc_point_buf);
const DRW_Attributes &attrs = cache->final.attr_used;
for (int i : IndexRange(attrs.num_requests)) {
const VectorSet<std::string> &attrs = cache->final.attr_used;
for (const int i : attrs.index_range()) {
/* Only refine point attributes. */
if (cache->proc_attributes_point_domain[i]) {
cache_update(cache->final.attributes_buf[i], cache->proc_attributes_buf[i]);
@@ -333,18 +333,18 @@ gpu::Batch *curves_sub_pass_setup_implementation(PassT &sub_ps,
CD_PROP_FLOAT2);
}
const DRW_Attributes &attrs = curves_cache->final.attr_used;
for (int i = 0; i < attrs.num_requests; i++) {
const DRW_AttributeRequest &request = attrs.requests[i];
const VectorSet<std::string> &attrs = curves_cache->final.attr_used;
for (const int i : attrs.index_range()) {
const StringRef name = attrs[i];
char sampler_name[32];
drw_curves_get_attribute_sampler_name(request.attribute_name, sampler_name);
drw_curves_get_attribute_sampler_name(name, sampler_name);
if (!curves_cache->proc_attributes_point_domain[i]) {
if (!curves_cache->proc_attributes_buf[i]) {
continue;
}
sub_ps.bind_texture(sampler_name, curves_cache->proc_attributes_buf[i]);
if (request.attribute_name == curve_data_render_uv) {
if (name == curve_data_render_uv) {
sub_ps.bind_texture("a", curves_cache->proc_attributes_buf[i]);
}
}
@@ -353,7 +353,7 @@ gpu::Batch *curves_sub_pass_setup_implementation(PassT &sub_ps,
continue;
}
sub_ps.bind_texture(sampler_name, curves_cache->final.attributes_buf[i]);
if (request.attribute_name == point_data_render_uv) {
if (name == point_data_render_uv) {
sub_ps.bind_texture("a", curves_cache->final.attributes_buf[i]);
}
}
@@ -362,7 +362,7 @@ gpu::Batch *curves_sub_pass_setup_implementation(PassT &sub_ps,
* we need to find the right index for this attribute as uniforms defining the scope of the
* attributes are based on attribute loading order, which is itself based on the material's
* attributes. */
const int index = attribute_index_in_material(gpu_material, request.attribute_name);
const int index = attribute_index_in_material(gpu_material, name);
if (index != -1) {
curves_infos.is_point_attribute[index][0] = curves_cache->proc_attributes_point_domain[i];
}

View File

@@ -9,10 +9,11 @@
#pragma once
#include <array>
#include <string>
#include "GPU_shader.hh"
#include "draw_attributes.hh"
#include "BLI_vector_set.hh"
struct Curves;
namespace blender::gpu {
@@ -48,13 +49,13 @@ struct CurvesEvalFinalCache {
int resolution;
/** Attributes currently being drawn or about to be drawn. */
DRW_Attributes attr_used;
VectorSet<std::string> attr_used;
/**
* Attributes that were used at some point. This is used for garbage collection, to remove
* attributes that are not used in shaders anymore due to user edits.
*/
DRW_Attributes attr_used_over_time;
VectorSet<std::string> attr_used_over_time;
/**
* The last time in seconds that the `attr_used` and `attr_used_over_time` were exactly the same.

View File

@@ -203,7 +203,7 @@ Vector<SculptBatch> sculpt_batches_per_material_get(const Object *ob,
BLI_assert(ob->type == OB_MESH);
const Mesh &mesh = DRW_object_get_data_for_drawing<Mesh>(*ob);
DRW_Attributes draw_attrs;
VectorSet<std::string> draw_attrs;
DRW_MeshCDMask cd_needed;
DRW_mesh_get_attributes(*ob, mesh, materials, &draw_attrs, &cd_needed);
@@ -212,9 +212,8 @@ Vector<SculptBatch> sculpt_batches_per_material_get(const Object *ob,
attrs.append(pbvh::CustomRequest::Position);
attrs.append(pbvh::CustomRequest::Normal);
for (int i = 0; i < draw_attrs.num_requests; i++) {
const DRW_AttributeRequest &req = draw_attrs.requests[i];
attrs.append(pbvh::GenericRequest(req.attribute_name));
for (const StringRef name : draw_attrs) {
attrs.append(pbvh::GenericRequest(name));
}
/* UV maps are not in attribute requests. */