diff --git a/luprex/core/cpp/animqueue.cpp b/luprex/core/cpp/animqueue.cpp index cb910a35..4570b7c8 100644 --- a/luprex/core/cpp/animqueue.cpp +++ b/luprex/core/cpp/animqueue.cpp @@ -301,10 +301,13 @@ void AnimQueue::add(int64_t id, const AnimStep &step) { } bool AnimQueue::valid() const { - // Animqueue must have at least one step. + // Animqueue must have at least one step, and no more than 255. if (steps_.empty()) { return false; } + if (steps_.size() > 255) { + return false; + } // First action should be blank. if (steps_[0].action_ != "") { return false; @@ -342,17 +345,19 @@ void AnimQueue::deserialize(StreamBuffer *sb) { } } -void AnimQueue::make_patch(const AnimQueue &auth, StreamBuffer *sb) const { +bool AnimQueue::make_patch(const AnimQueue &auth, StreamBuffer *sb) const { // Sanity check. assert(valid()); assert(auth.valid()); // Special case: if we're already a perfect match, output 0 bytes. if (exactly_equal(auth)) { - return; + sb->write_uint8(0); + return false; } // Write the first element. + sb->write_uint8(auth.steps_.size()); const AnimStep &first = auth.steps_[0]; int match = 0; while ((match < int(steps_.size())) && (!steps_[match].state_equal(first))) { @@ -388,18 +393,19 @@ void AnimQueue::make_patch(const AnimQueue &auth, StreamBuffer *sb) const { } } } + return true; } void AnimQueue::apply_patch(StreamBuffer *sb) { - // Special case: if there are zero bytes, no patch is needed. - if (sb->at_eof()) { + int len = sb->read_uint8(); + if (len == 0) { return; } // Decode the diff, stop at eof. std::deque old = std::move(steps_); steps_.clear(); - while (!sb->at_eof()) { + for (int i = 0; i < len; i++) { uint8_t index = sb->read_uint8(); if (index < 255) { assert(index < old.size()); @@ -412,9 +418,10 @@ void AnimQueue::apply_patch(StreamBuffer *sb) { int size = steps_.size(); if (size > 1) { steps_[size-1].echo(steps_[size-2]); + } else { + steps_[0].keep_state_only(); } } - steps_.front().keep_state_only(); } const AnimStep &AnimQueue::back() const { @@ -559,7 +566,7 @@ LuaDefine(unittests_animqueue, "c") { aq.add(12345, stp); sb.clear(); - aqds.make_patch(aq, &sb); + LuaAssert(L, aqds.make_patch(aq, &sb)); aqds.apply_patch(&sb); LuaAssert(L, aqds.exactly_equal(aq)); @@ -570,8 +577,19 @@ LuaDefine(unittests_animqueue, "c") { stp.set_plane("where"); aq.add(232, stp); + // Generate diffs, but add 4 extra bytes. sb.clear(); - aqds.make_patch(aq, &sb); + LuaAssert(L, aqds.make_patch(aq, &sb)); + sb.write_uint32(0); + + // Apply the diffs, 4 extra bytes should remain. + aqds.apply_patch(&sb); + LuaAssert(L, sb.write_count() - sb.read_count() == 4); + LuaAssert(L, aqds.exactly_equal(aq)); + + // compare again, should be no differences. + sb.clear(); + LuaAssert(L, !aqds.make_patch(aq, &sb)); aqds.apply_patch(&sb); LuaAssert(L, aqds.exactly_equal(aq)); @@ -579,7 +597,7 @@ LuaDefine(unittests_animqueue, "c") { aq.keep_only(1); sb.clear(); - aqds.make_patch(aq, &sb); + LuaAssert(L, aqds.make_patch(aq, &sb)); aqds.apply_patch(&sb); LuaAssert(L, aqds.exactly_equal(aq)); diff --git a/luprex/core/cpp/animqueue.hpp b/luprex/core/cpp/animqueue.hpp index 721005cd..9a38747f 100644 --- a/luprex/core/cpp/animqueue.hpp +++ b/luprex/core/cpp/animqueue.hpp @@ -168,11 +168,10 @@ public: // Difference transmission // - // make_patch will emit zero bytes when there are no differences. - // make_patch does not put a 'length' field for the patch as a whole. - // apply_patch must receive a patch followed by eof. + // When there are no differences, make_patch returns false but + // still writes out a valid patch. // - void make_patch(const AnimQueue &auth, StreamBuffer *sb) const; + bool make_patch(const AnimQueue &auth, StreamBuffer *sb) const; void apply_patch(StreamBuffer *sb); // Get the final resting place after all animations are complete. diff --git a/luprex/core/cpp/idalloc.cpp b/luprex/core/cpp/idalloc.cpp index 934adac2..ce121e65 100644 --- a/luprex/core/cpp/idalloc.cpp +++ b/luprex/core/cpp/idalloc.cpp @@ -1,5 +1,6 @@ #include "idalloc.hpp" #include +#include static int64_t nthbatch(int64_t n) { @@ -121,7 +122,15 @@ void IdPlayerPool::disable_fifo() { fifo_enabled_ = false; } -void IdPlayerPool::purge() { +void IdPlayerPool::test_push_back(int64_t range) { + ranges_.push_back(range); +} + +void IdPlayerPool::test_pop_front() { + ranges_.pop_front(); +} + +void IdPlayerPool::test_clear_ranges() { ranges_.clear(); } @@ -184,6 +193,96 @@ void IdPlayerPool::deserialize(StreamBuffer *sb) { } } +bool IdPlayerPool::exactly_equal(const IdPlayerPool &other) const { + if (fifo_enabled_ != other.fifo_enabled_) return false; + if (ranges_.size() != other.ranges_.size()) return false; + for (int i = 0; i < int(ranges_.size()); i++) { + if (ranges_[i] != other.ranges_[i]) { + return false; + } + } + return true; +} + +bool IdPlayerPool::valid() const { + if ((!fifo_enabled_) && (ranges_.size() > 0)) return false; + if (ranges_.size() > 250) return false; + return true; +} + +bool IdPlayerPool::make_patch(const IdPlayerPool &auth, StreamBuffer *sb) const { + assert(valid()); + assert(auth.valid()); + assert(global_->queue_fill() == auth.global_->queue_fill()); + + // The first byte. + // 0 no differences + // 1 fifo disabled and ranges empty + // 2+ fifo enabled and N-2 ranges. + // + if (exactly_equal(auth)) { + sb->write_uint8(0); + return false; + } + + if (!auth.fifo_enabled_) { + sb->write_uint8(1); + return true; + } + + sb->write_uint8(auth.ranges_.size() + 2); + + // Build up an index of the known IDs. + std::map index; + for (int i = 0; i < int(ranges_.size()); i++) { + index[ranges_[i]] = i; + } + + + // Write the ranges, but encode known IDs in one byte. + for (int i = 0; i < int(auth.ranges_.size()); i++) { + int64_t n = auth.ranges_[i]; + auto iter = index.find(n); + if (iter == index.end()) { + sb->write_uint8(255); + sb->write_int64(n); + } else { + int slot = iter->second; + sb->write_uint8(slot); + } + } + return true; +} + +void IdPlayerPool::apply_patch(StreamBuffer *sb) { + // read the header byte + int nranges = sb->read_uint8(); + if (nranges == 0) { + return; + } + + if (nranges == 1) { + fifo_enabled_ = false; + ranges_.clear(); + return; + } + + fifo_enabled_ = true; + nranges -= 2; + + std::deque old = std::move(ranges_); + ranges_.clear(); + for (int i = 0; i < nranges; i++) { + int index = sb->read_uint8(); + if (index < 255) { + assert(index < int(old.size())); + ranges_.push_back(old[index]); + } else { + ranges_.push_back(sb->read_int64()); + } + } +} + LuaDefine(unittests_idalloc, "c") { IdGlobalPool gp; IdPlayerPool pp(&gp); @@ -244,7 +343,7 @@ LuaDefine(unittests_idalloc, "c") { LuaAssert(L, gp.get_batch() == nthbatch(2)); // In the synchronous model, refill should do nothing. - pp.purge(); + pp.test_clear_ranges(); pp.enable_fifo(); gp.init_synch(3); pp.refill(); @@ -255,7 +354,7 @@ LuaDefine(unittests_idalloc, "c") { // In the master model, with fifo disabled. Fifo should remain // empty, but batches should be returned. - pp.purge(); + pp.test_clear_ranges(); pp.disable_fifo(); gp.init_master(3); pp.refill(); @@ -265,7 +364,7 @@ LuaDefine(unittests_idalloc, "c") { LuaAssert(L, pp.get_batch() == nthbatch(1)); // Test refill from master (with enabled fifo). - pp.purge(); + pp.test_clear_ranges(); pp.enable_fifo(); gp.init_master(3); pp.refill(); @@ -285,7 +384,7 @@ LuaDefine(unittests_idalloc, "c") { LuaAssert(L, gp.get_batch() == nthbatch(6)); // Try preparing a thread and salvaging a thread. - pp.purge(); + pp.test_clear_ranges(); pp.enable_fifo(); gp.init_master(3); lua_setnextid(L, 0); @@ -328,12 +427,12 @@ LuaDefine(unittests_idalloc, "c") { gp.init_master(3); gpds.init_synch(5); LuaAssert(L, gp.get_batch() == nthbatch(0)); - pp.purge(); + pp.test_clear_ranges(); pp.enable_fifo(); pp.refill(); LuaAssert(L, pp.fifo_enabled()); LuaAssert(L, pp.size() == 3); - ppds.purge(); + ppds.test_clear_ranges(); pp.serialize(&sb); ppds.deserialize(&sb); LuaAssert(L, ppds.fifo_enabled()); @@ -342,5 +441,41 @@ LuaDefine(unittests_idalloc, "c") { LuaAssert(L, ppds.get_batch() == nthbatch(2)); LuaAssert(L, ppds.get_batch() == nthbatch(3)); + // Difference transmit compare two empty pools. + gp.init_master(3); + gpds.init_master(3); + pp.test_clear_ranges(); + ppds.test_clear_ranges(); + pp.enable_fifo(); + ppds.enable_fifo(); + + // Check case: no differences. + sb.clear(); + LuaAssert(L, !ppds.make_patch(pp, &sb)); + ppds.apply_patch(&sb); + LuaAssert(L, ppds.exactly_equal(pp)); + + // Add some values to master pool + pp.test_push_back(123); + pp.test_push_back(456); + + // transmit and compare. Add extra bytes + sb.clear(); + LuaAssert(L, ppds.make_patch(pp, &sb)); + sb.write_uint32(0); + ppds.apply_patch(&sb); + LuaAssert(L, sb.write_count() - sb.read_count() == 4); + LuaAssert(L, ppds.exactly_equal(pp)); + + // Pop a value from master pool + pp.test_pop_front(); + pp.test_push_back(789); + + // transmit and compare. + sb.clear(); + LuaAssert(L, ppds.make_patch(pp, &sb)); + ppds.apply_patch(&sb); + LuaAssert(L, ppds.exactly_equal(pp)); + return 0; } diff --git a/luprex/core/cpp/idalloc.hpp b/luprex/core/cpp/idalloc.hpp index 557522e8..52347e73 100644 --- a/luprex/core/cpp/idalloc.hpp +++ b/luprex/core/cpp/idalloc.hpp @@ -153,9 +153,6 @@ public: // Refill the fifo of batches from the global pool. void refill(); - // Discard all batches in the fifo. This is only for unit testing. - void purge(); - // Get a batch from the fifo. Also refills the fifo. int64_t get_batch(); @@ -171,10 +168,33 @@ public: // Return the size of the queue. int size() { return ranges_.size(); } + // Return true if the two pools are identical. + bool exactly_equal(const IdPlayerPool &other) const; + + // Check that the pool is valid. + bool valid() const; + + // unit testing functions. + // + // These let you manipulate the deque explicitly. But that's + // only useful for unit testing. + // + void test_push_back(int64_t range); + void test_pop_front(); + void test_clear_ranges(); + // Serialize to or deserialize from a streambuffer. // Caution: the pointer to the global pool is not serialized or deserialized. void serialize(StreamBuffer *sb); void deserialize(StreamBuffer *sb); + + // Difference transmission + // + // When there are no differences, make_patch returns false but + // still writes out a valid patch. + // + bool make_patch(const IdPlayerPool &auth, StreamBuffer *sb) const; + void apply_patch(StreamBuffer *sb); }; #endif // IDALLOC_HPP