Skip to content

Update NO_REPEAT_RANDOM to use Sample Without Replacement (2nd Try)#853

Open
pedker wants to merge 8 commits intoprofezzorn:masterfrom
pedker:sample-without-replacement
Open

Update NO_REPEAT_RANDOM to use Sample Without Replacement (2nd Try)#853
pedker wants to merge 8 commits intoprofezzorn:masterfrom
pedker:sample-without-replacement

Conversation

@pedker
Copy link
Contributor

@pedker pedker commented Sep 17, 2025

This is a second try at this PR, attempting to address its raised issues.

The storage used to manage the Sample-Without-Replacement algorithm is now a one-dimensional boolean array on each Effect, created statically of size NO_REPEAT_RANDOM_BUFFER_SIZE_BITS. Each bit of the array is used to manage an individual Sound File, and the array's size determines the maximum number of Sound Files each Effect can support using SWR. If an Effect has more Sound Files than available memory, then the Effect will default back to using the existing NO_REPEAT_RANDOM logic.

To meet the requirement of using less than one byte of memory per File, the value of NO_REPEAT_RANDOM_BUFFER_SIZE_BITS should be tuned relative to the ratio of Files to Effects used by Fonts on average. For example, setting it to 16 would only require the ratio to be 2:1, A value of 24 would require 3:1, and a value of 32 would require 4:1.

@pedker pedker marked this pull request as ready for review September 17, 2025 05:47
#ifdef KILL_OLD_PLAYERS
killable_ = false;
#endif
#ifdef NO_REPEAT_RANDOM
Copy link
Owner

Choose a reason for hiding this comment

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

If we're going to have this code (which I'm not entirely convinced it is a good idea.) then we should use a new define to enable it. Maybe the NO_REPEAT_RANDOM_BUFFER_SIZE_BITS define?

sound/effect.h Outdated

#ifdef NO_REPEAT_RANDOM
// Storage for random sampling without replacement.
bool available_[NO_REPEAT_RANDOM_BUFFER_SIZE_BITS];
Copy link
Owner

Choose a reason for hiding this comment

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

An array of bool does not take one bit per bool.
In most cases, a bool takes one byte.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, crap! I incorrectly assumed that a boolean took up one bit in memory. I will have to look into the feasibility of refactoring to instead use a single integer with bitmasking.

This means that available_ will change to be either of type uint16_t or uint32_t, and the size will no longer be configurable. The current version of the define macro will be renamed/reused as the enable define, mentioned above. 32 bits is close to too much memory, considering average file counts. 16 bits is way below the budget, but a max of 16 files per Effect may feel a bit constraining. I am leaning towards 16 bits and will procced that way unless told otherwise.

Copy link
Owner

Choose a reason for hiding this comment

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

You could use this class:

class BitSet {

sound/effect.h Outdated
#ifdef NO_REPEAT_RANDOM
// Storage for random sampling without replacement.
bool available_[NO_REPEAT_RANDOM_BUFFER_SIZE_BITS];
bool available_mark_ = false;
Copy link
Owner

Choose a reason for hiding this comment

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

This needs a better name, or a comment that explains what it is for.

sound/effect.h Outdated
for (uint8_t i = 0; i < total_files; ++i) {
const int16_t file = sub_files_ ? i / sub_files_ : i;
const int16_t subfile = sub_files_ ? i % sub_files_ : 0;
n += available_[i] == available_mark_ && (last_ != file || (sub_files_ && last_subid_ != subfile));
Copy link
Owner

Choose a reason for hiding this comment

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

Use an if statement here, it will make the code easier to read.

sound/effect.h Outdated
n += available_[i] == available_mark_ && (last_ != file || (sub_files_ && last_subid_ != subfile));
}
if (!n) {
available_mark_ = !available_mark_;
Copy link
Owner

Choose a reason for hiding this comment

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

Using available_mark_ this way doesn't save anything if you have to iterate over the whole array to check how many elements are available anyways.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It lowers the worst case from 3N to 2N, since the alternative is to iterate over the array one more time to mark everything as available. I agree though, it's not really much of a save since it's still all O(N). The refactor will scrap this anyway, since if available_ is becoming an integer I can just set it back to 0 or max

Copy link
Owner

Choose a reason for hiding this comment

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

Marking everything as available only has to be done every Nth time though, so the average time spent ends up being constant.

@pedker
Copy link
Contributor Author

pedker commented Oct 2, 2025

Successfully converted this to use a BitSet, though I had to add a few functions to the BitSet class, mainly to support some subset-based operations. Also, I set the memory size to 32 because BitSets denominate by increments of 32-bit ints, so setting the size to 16 would just waste the other 16 bits.

Something else to note is that I have not been able to test the random_subid() portion of this code, because I cannot get that function to fire under normal circumstances. It's supposed to be called by GetFollowing()...

}
return ret;
}
size_t popcount_subset(size_t first, size_t last) const {
Copy link
Owner

Choose a reason for hiding this comment

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

I'm wondering if it would be easier (and faster) to do all this subset stuff with ands and ors.

Basically something like:

 Bitset subset = ~(Bitset::All() << last) & (Bitset::All() << first);
 size_t popcount_subset = (subset & bitset).popcount();

sound/effect.h Outdated
uint8_t n = available_.popcount();
if (!n) {
available_.fill(total_files);
n = available_.popcount();
Copy link
Owner

Choose a reason for hiding this comment

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

n = total_files; would be faster

bits_[i] = 0;
}
}
void fill(size_t bit = SIZE) {
Copy link
Owner

Choose a reason for hiding this comment

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

This should be a static function that returns a bitset. It also needs a comment to explain what it does.

}
}
void fill(size_t bit = SIZE) {
for (size_t i = 0; i < NELEM(bits_); i++) {
Copy link
Owner

Choose a reason for hiding this comment

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

The loop should only iterate over all bits_[] where it can assign 0xFFFFFFFF, then assign the final value after the loop.

@pedker
Copy link
Contributor Author

pedker commented Oct 11, 2025

Needed to add some bitwise operator overloads to support the proposed changes, figured I'd take a shot at making them all while I'm there (except XOR). I'm considering renaming some or all of the instances of subset to instead be range, but I'm on the fence.

ret >>= bits;
return ret;
}
void operator<<=(int bits) {
Copy link
Owner

Choose a reason for hiding this comment

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

We should probably improve these while we're at it, something like:

uint32_t get_word(int word) {
  if (word < 0 || word >= (int) NELEM(bits_)) return 0;
  return bits_[word];
}
uint32_t get32(int pos) {
  uint64_t tmp = get_word(1 + (pos >> 5));
  tmp <<= 32;
  tmp |= get_word(pos >> 5); 
  return tmp >> (pos & 31); 
}
BitSet<SIZE> operator>>(int bits) const {
   BitSet<SIZE> ret;
   for (int i = 0; i < SIZE; i++) ret.bits_[i]= get32(i*32 + bits);
   return ret;
}

<< would be basically the same, but with a - inside the get32() call.

@pedker
Copy link
Contributor Author

pedker commented Oct 16, 2025

I've implemented the suggestions for the shifters. It compiles, but I currently do not have time to thoroughly test it as I am out of town soon. I will do thorough testing when I return, but on paper this looks pretty good to me

@pedker
Copy link
Contributor Author

pedker commented Oct 25, 2025

Identified a crashing issue and put up a fix. Unfortunately, before I could verify the fix and finish testing, I accidentally ripped the USB port right off of the ProffieBoard of my saber. So, for now, my ability to work on this has been crippled. I'm not sure how long it will take to remedy, as I may have to contact support, try to DIY fix/replace the board on my own, or wait until I can afford to pay for a replacement solution.

@pedker
Copy link
Contributor Author

pedker commented Nov 8, 2025

I have replaced my soundboard! I've done more extensive testing now, everything is behaving as expected now. I was also able to test calling random_subid() and it now works as I expect.

}
void operator<<=(int bits) {
if (!bits) return;
const size_t n = NELEM(bits_);
Copy link
Owner

Choose a reason for hiding this comment

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

This looks like a complicated way to write:

for (int i = NELEM(bits_)-1; i >= 0; i--) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair, I was going to but was afraid of moving away from using size_t

@pedker
Copy link
Contributor Author

pedker commented Nov 9, 2025

I also caught something I did wrong when attempting to address an edge case in fill(), the real issue was that the N input is not 0-indexed, but was being used to access the BitSet without accounting for 0-indexing

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.

2 participants